Skip to content

Commit

Permalink
Dexie Cloud: Support for unsynced properties (local-only properties t…
Browse files Browse the repository at this point in the history
…hat are never synced to server)
  • Loading branch information
David Fahlander committed Oct 17, 2024
1 parent d8f9263 commit da98d8f
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 14 deletions.
4 changes: 4 additions & 0 deletions addons/dexie-cloud/src/DexieCloudOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export interface DexieCloudOptions {
// not be synced with Dexie Cloud
unsyncedTables?: string[];

unsyncedProperties?: {
[tableName: string]: string[];
}

// By default Dexie Cloud will suffix the cloud DB ID to your IndexedDB database name
// in order to ensure that the local database is uniquely tied to the remote one and
// will use another local database if databaseURL is changed or if dexieCloud addon
Expand Down
5 changes: 2 additions & 3 deletions addons/dexie-cloud/src/define-ydoc-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,8 @@ const createMiddleware: (db: Dexie) => Middleware<DBCore> = (db) => ({
mutate(req) {
switch (req.type) {
case 'add': {
for (const obj of req.values) {
const primaryKey =
coreTable.schema.primaryKey.extractKey!(obj);
for (const yUpdateRow of req.values) {
const primaryKey = (yUpdateRow as YUpdateRow).k;
const doc = DexieYProvider.getDocCache(db).find(
parentTable,
primaryKey,
Expand Down
116 changes: 105 additions & 11 deletions addons/dexie-cloud/src/middlewares/createMutationTrackingMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
DBCoreTable,
DBCoreTransaction,
Middleware,
RangeSet,
} from 'dexie';
import { DBOperation } from 'dexie-cloud-common';
import { DBOperation, DBUpdateOperation } from 'dexie-cloud-common';
import { BehaviorSubject } from 'rxjs';
import { DexieCloudDB } from '../db/DexieCloudDB';
import { UserLogin } from '../db/entities/UserLogin';
Expand Down Expand Up @@ -94,10 +95,7 @@ export function createMutationTrackingMiddleware({
outstandingTransactions.next(outstandingTransactions.value);
};
const txComplete = () => {
if (
tx.mutationsAdded &&
!isEagerSyncDisabled(db)
) {
if (tx.mutationsAdded && !isEagerSyncDisabled(db)) {
triggerSync(db, 'push');
}
removeTransaction();
Expand Down Expand Up @@ -193,27 +191,98 @@ export function createMutationTrackingMiddleware({
req: DBCoreDeleteRequest | DBCoreAddRequest | DBCorePutRequest
): Promise<DBCoreMutateResponse> {
const trans = req.trans as DBCoreTransaction & TXExpandos;
trans.mutationsAdded = true;
const unsyncedProps =
db.cloud.options?.unsyncedProperties?.[tableName];
const {
txid,
currentUser: { userId },
} = trans;
const { type } = req;
const opNo = ++trans.opCount;

function stripChangeSpec(changeSpec: { [keyPath: string]: any }) {
if (!unsyncedProps) return changeSpec;
let rv = changeSpec;
for (const keyPath of Object.keys(changeSpec)) {
if (
unsyncedProps.some(
(p) => keyPath === p || keyPath.startsWith(p + '.')
)
) {
if (rv === changeSpec) rv = { ...changeSpec }; // clone on demand
delete rv[keyPath];
}
}
return rv;
}

return table.mutate(req).then((res) => {
const { numFailures: hasFailures, failures } = res;
let keys = type === 'delete' ? req.keys! : res.results!;
let values = 'values' in req ? req.values : [];
let updates = 'updates' in req && req.updates!;
let changeSpec = 'changeSpec' in req ? req.changeSpec : undefined;
let updates = 'updates' in req ? req.updates : undefined;

if (hasFailures) {
keys = keys.filter((_, idx) => !failures[idx]);
values = values.filter((_, idx) => !failures[idx]);
}
if (unsyncedProps) {
// Filter out unsynced properties
values = values.map((value) => {
const newValue = { ...value };
for (const prop of unsyncedProps) {
delete newValue[prop];
}
return newValue;
});
if (changeSpec) {
// modify operation with criteria and changeSpec.
// We must strip out unsynced properties from changeSpec.
// We deal with criteria later.
changeSpec = stripChangeSpec(changeSpec);
if (Object.keys(changeSpec).length === 0) {
// Nothing to change on server
return res;
}
}
if (updates) {
let strippedChangeSpecs =
updates.changeSpecs.map(stripChangeSpec);
let newUpdates: DBCorePutRequest['updates'] = {
keys: [],
changeSpecs: [],
};
const validKeys = new RangeSet();
let anyChangeSpecBecameEmpty = false;
for (let i = 0, l = strippedChangeSpecs.length; i < l; ++i) {
if (Object.keys(strippedChangeSpecs[i]).length > 0) {
newUpdates.keys.push(updates.keys[i]);
newUpdates.changeSpecs.push(strippedChangeSpecs[i]);
validKeys.addKey(updates.keys[i]);
} else {
anyChangeSpecBecameEmpty = true;
}
}
updates = newUpdates;
if (anyChangeSpecBecameEmpty) {
// Some keys were stripped. We must also strip them from keys and values
let newKeys: any[] = [];
let newValues: any[] = [];
for (let i = 0, l = keys.length; i < l; ++i) {
if (validKeys.hasKey(keys[i])) {
newKeys.push(keys[i]);
newValues.push(values[i]);
}
}
keys = newKeys;
values = newValues;
}
}
}
const ts = Date.now();

// Canonicalize req.criteria.index to null if it's on the primary key.
const criteria =
let criteria =
'criteria' in req && req.criteria
? {
...req.criteria,
Expand All @@ -223,6 +292,20 @@ export function createMutationTrackingMiddleware({
: req.criteria.index,
}
: undefined;
if (unsyncedProps && criteria?.index) {
const keyPaths = schema.indexes.find(
(idx) => idx.name === criteria!.index
)?.keyPath;
const involvedProps = keyPaths
? typeof keyPaths === 'string'
? [keyPaths]
: keyPaths
: [];
if (involvedProps.some((p) => unsyncedProps?.includes(p))) {
// Don't log criteria on unsynced properties as the server could not test them.
criteria = undefined;
}
}

const mut: DBOperation =
req.type === 'delete'
Expand All @@ -245,15 +328,26 @@ export function createMutationTrackingMiddleware({
userId,
values,
}
: criteria && req.changeSpec
: criteria && changeSpec
? {
// Common changeSpec for all keys
type: 'modify',
ts,
opNo,
keys,
criteria,
changeSpec: req.changeSpec,
changeSpec,
txid,
userId,
}
: changeSpec
? {
// In case criteria involved an unsynced property, we go for keys instead.
type: 'update',
ts,
opNo,
keys,
changeSpecs: keys.map(() => changeSpec!),
txid,
userId,
}
Expand Down

0 comments on commit da98d8f

Please sign in to comment.