@syncular/client 0.0.6-144 → 0.0.6-152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -1
- package/dist/create-client.d.ts +2 -0
- package/dist/create-client.d.ts.map +1 -1
- package/dist/create-client.js +1 -0
- package/dist/create-client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +5 -0
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +91 -7
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +2 -0
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/mutations.d.ts +3 -0
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +114 -10
- package/dist/mutations.js.map +1 -1
- package/dist/plugins/types.d.ts +26 -1
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +7 -0
- package/src/create-client.ts +3 -0
- package/src/engine/SyncEngine.ts +124 -10
- package/src/engine/types.ts +2 -0
- package/src/mutations.ts +161 -28
- package/src/plugins/types.ts +38 -0
package/src/mutations.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type { Insertable, Kysely, Transaction, Updateable } from 'kysely';
|
|
|
31
31
|
import { sql } from 'kysely';
|
|
32
32
|
import { enqueueOutboxCommit } from './outbox';
|
|
33
33
|
import type {
|
|
34
|
+
SyncClientLocalMutationArgs,
|
|
34
35
|
SyncClientPlugin,
|
|
35
36
|
SyncClientPluginContext,
|
|
36
37
|
} from './plugins/types';
|
|
@@ -402,6 +403,9 @@ export interface OutboxCommitConfig<DB extends SyncClientDb> {
|
|
|
402
403
|
omitColumns?: string[];
|
|
403
404
|
codecs?: ColumnCodecSource;
|
|
404
405
|
codecDialect?: ColumnCodecDialect;
|
|
406
|
+
plugins?: SyncClientPlugin[];
|
|
407
|
+
actorId?: string;
|
|
408
|
+
clientId?: string;
|
|
405
409
|
}
|
|
406
410
|
|
|
407
411
|
export function createOutboxCommit<DB extends SyncClientDb>(
|
|
@@ -411,6 +415,11 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
411
415
|
const versionColumn = config.versionColumn ?? 'server_version';
|
|
412
416
|
const omitColumns = config.omitColumns ?? [];
|
|
413
417
|
const codecDialect = config.codecDialect ?? 'sqlite';
|
|
418
|
+
const sortedPlugins = sortPlugins(config.plugins ?? []);
|
|
419
|
+
const pluginContext: SyncClientPluginContext = {
|
|
420
|
+
actorId: config.actorId ?? 'unknown',
|
|
421
|
+
clientId: config.clientId ?? 'unknown',
|
|
422
|
+
};
|
|
414
423
|
|
|
415
424
|
return async (fn) => {
|
|
416
425
|
const operations: SyncOperation[] = [];
|
|
@@ -420,6 +429,42 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
420
429
|
op: SyncOpKind;
|
|
421
430
|
}> = [];
|
|
422
431
|
|
|
432
|
+
const transformLocalOperation = async (
|
|
433
|
+
operation: SyncOperation
|
|
434
|
+
): Promise<SyncOperation> => {
|
|
435
|
+
if (sortedPlugins.length === 0) return operation;
|
|
436
|
+
|
|
437
|
+
const transformed = await runBeforeApplyLocalMutationsPlugins({
|
|
438
|
+
plugins: sortedPlugins,
|
|
439
|
+
ctx: pluginContext,
|
|
440
|
+
operations: [operation],
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (transformed.length !== 1) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
'beforeApplyLocalMutations must return exactly one operation per input operation'
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const [nextOperation] = transformed;
|
|
450
|
+
if (!nextOperation) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
'beforeApplyLocalMutations returned an empty operation'
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (
|
|
456
|
+
nextOperation.table !== operation.table ||
|
|
457
|
+
nextOperation.row_id !== operation.row_id ||
|
|
458
|
+
nextOperation.op !== operation.op
|
|
459
|
+
) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
'beforeApplyLocalMutations cannot change operation table, row_id, or op'
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return nextOperation;
|
|
466
|
+
};
|
|
467
|
+
|
|
423
468
|
const { result, receipt } = await config.db
|
|
424
469
|
.transaction()
|
|
425
470
|
.execute(async (trx) => {
|
|
@@ -469,14 +514,6 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
469
514
|
typeof rawId === 'string' && rawId ? rawId : randomId();
|
|
470
515
|
|
|
471
516
|
const row = { ...raw, [idColumn]: id };
|
|
472
|
-
const dbRow = applyCodecsToDbRow(
|
|
473
|
-
row,
|
|
474
|
-
resolveTableCodecs(table, row),
|
|
475
|
-
codecDialect
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
await dynamicInsert(trx, table, dbRow);
|
|
479
|
-
|
|
480
517
|
const payload = sanitizePayload(row, {
|
|
481
518
|
omit: [
|
|
482
519
|
idColumn,
|
|
@@ -484,6 +521,27 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
484
521
|
...omitColumns,
|
|
485
522
|
],
|
|
486
523
|
});
|
|
524
|
+
const localSanitized = sanitizePayload(row, {
|
|
525
|
+
omit: [idColumn, ...(versionColumn ? [versionColumn] : [])],
|
|
526
|
+
});
|
|
527
|
+
const localOp = await transformLocalOperation({
|
|
528
|
+
table,
|
|
529
|
+
row_id: id,
|
|
530
|
+
op: 'upsert',
|
|
531
|
+
payload: localSanitized,
|
|
532
|
+
base_version: null,
|
|
533
|
+
});
|
|
534
|
+
const localPayload = isRecord(localOp.payload)
|
|
535
|
+
? localOp.payload
|
|
536
|
+
: {};
|
|
537
|
+
const localRow = { ...localPayload, [idColumn]: id };
|
|
538
|
+
const dbRow = applyCodecsToDbRow(
|
|
539
|
+
localRow,
|
|
540
|
+
resolveTableCodecs(table, localRow),
|
|
541
|
+
codecDialect
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
await dynamicInsert(trx, table, dbRow);
|
|
487
545
|
|
|
488
546
|
operations.push({
|
|
489
547
|
table: table,
|
|
@@ -503,6 +561,7 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
503
561
|
}
|
|
504
562
|
const ids: string[] = [];
|
|
505
563
|
const toInsert: Record<string, unknown>[] = [];
|
|
564
|
+
const toLocalInsert: Record<string, unknown>[] = [];
|
|
506
565
|
|
|
507
566
|
for (const values of rows) {
|
|
508
567
|
const raw = isRecord(values) ? values : {};
|
|
@@ -513,18 +572,6 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
513
572
|
toInsert.push({ ...raw, [idColumn]: id });
|
|
514
573
|
}
|
|
515
574
|
|
|
516
|
-
const dbRows = toInsert.map((row) =>
|
|
517
|
-
applyCodecsToDbRow(
|
|
518
|
-
row,
|
|
519
|
-
resolveTableCodecs(table, row),
|
|
520
|
-
codecDialect
|
|
521
|
-
)
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
if (dbRows.length > 0) {
|
|
525
|
-
await dynamicInsert(trx, table, dbRows);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
575
|
for (let i = 0; i < toInsert.length; i++) {
|
|
529
576
|
const row = toInsert[i]!;
|
|
530
577
|
const id = ids[i]!;
|
|
@@ -535,6 +582,20 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
535
582
|
...omitColumns,
|
|
536
583
|
],
|
|
537
584
|
});
|
|
585
|
+
const localSanitized = sanitizePayload(row, {
|
|
586
|
+
omit: [idColumn, ...(versionColumn ? [versionColumn] : [])],
|
|
587
|
+
});
|
|
588
|
+
const localOp = await transformLocalOperation({
|
|
589
|
+
table,
|
|
590
|
+
row_id: id,
|
|
591
|
+
op: 'upsert',
|
|
592
|
+
payload: localSanitized,
|
|
593
|
+
base_version: null,
|
|
594
|
+
});
|
|
595
|
+
const localPayload = isRecord(localOp.payload)
|
|
596
|
+
? localOp.payload
|
|
597
|
+
: {};
|
|
598
|
+
toLocalInsert.push({ ...localPayload, [idColumn]: id });
|
|
538
599
|
|
|
539
600
|
operations.push({
|
|
540
601
|
table: table,
|
|
@@ -547,6 +608,18 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
547
608
|
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
548
609
|
}
|
|
549
610
|
|
|
611
|
+
const dbRows = toLocalInsert.map((row) =>
|
|
612
|
+
applyCodecsToDbRow(
|
|
613
|
+
row,
|
|
614
|
+
resolveTableCodecs(table, row),
|
|
615
|
+
codecDialect
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
if (dbRows.length > 0) {
|
|
620
|
+
await dynamicInsert(trx, table, dbRows);
|
|
621
|
+
}
|
|
622
|
+
|
|
550
623
|
return ids;
|
|
551
624
|
},
|
|
552
625
|
|
|
@@ -559,12 +632,25 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
559
632
|
...omitColumns,
|
|
560
633
|
],
|
|
561
634
|
});
|
|
635
|
+
const localSanitized = sanitizePayload(rawPatch, {
|
|
636
|
+
omit: [idColumn, ...(versionColumn ? [versionColumn] : [])],
|
|
637
|
+
});
|
|
562
638
|
|
|
563
639
|
const hasExplicitBaseVersion =
|
|
564
640
|
!!opts && hasOwn(opts, 'baseVersion');
|
|
641
|
+
const localOp = await transformLocalOperation({
|
|
642
|
+
table,
|
|
643
|
+
row_id: id,
|
|
644
|
+
op: 'upsert',
|
|
645
|
+
payload: localSanitized,
|
|
646
|
+
base_version: null,
|
|
647
|
+
});
|
|
648
|
+
const localPayload = isRecord(localOp.payload)
|
|
649
|
+
? localOp.payload
|
|
650
|
+
: {};
|
|
565
651
|
const dbPatch = applyCodecsToDbRow(
|
|
566
|
-
|
|
567
|
-
resolveTableCodecs(table,
|
|
652
|
+
localPayload,
|
|
653
|
+
resolveTableCodecs(table, localPayload),
|
|
568
654
|
codecDialect
|
|
569
655
|
);
|
|
570
656
|
|
|
@@ -630,12 +716,25 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
630
716
|
...omitColumns,
|
|
631
717
|
],
|
|
632
718
|
});
|
|
719
|
+
const localSanitized = sanitizePayload(rawPatch, {
|
|
720
|
+
omit: [idColumn, ...(versionColumn ? [versionColumn] : [])],
|
|
721
|
+
});
|
|
633
722
|
|
|
634
723
|
const hasExplicitBaseVersion =
|
|
635
724
|
!!opts && hasOwn(opts, 'baseVersion');
|
|
725
|
+
const localOp = await transformLocalOperation({
|
|
726
|
+
table,
|
|
727
|
+
row_id: id,
|
|
728
|
+
op: 'upsert',
|
|
729
|
+
payload: localSanitized,
|
|
730
|
+
base_version: null,
|
|
731
|
+
});
|
|
732
|
+
const localPayload = isRecord(localOp.payload)
|
|
733
|
+
? localOp.payload
|
|
734
|
+
: {};
|
|
636
735
|
const dbPatch = applyCodecsToDbRow(
|
|
637
|
-
|
|
638
|
-
resolveTableCodecs(table,
|
|
736
|
+
localPayload,
|
|
737
|
+
resolveTableCodecs(table, localPayload),
|
|
639
738
|
codecDialect
|
|
640
739
|
);
|
|
641
740
|
|
|
@@ -732,6 +831,43 @@ function clonePushRequest(request: SyncPushRequest): SyncPushRequest {
|
|
|
732
831
|
return JSON.parse(JSON.stringify(request)) as SyncPushRequest;
|
|
733
832
|
}
|
|
734
833
|
|
|
834
|
+
function cloneLocalMutationArgs(
|
|
835
|
+
args: SyncClientLocalMutationArgs
|
|
836
|
+
): SyncClientLocalMutationArgs {
|
|
837
|
+
if (typeof structuredClone === 'function') {
|
|
838
|
+
return structuredClone(args);
|
|
839
|
+
}
|
|
840
|
+
return JSON.parse(JSON.stringify(args)) as SyncClientLocalMutationArgs;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function sortPlugins(plugins: readonly SyncClientPlugin[]): SyncClientPlugin[] {
|
|
844
|
+
return [...plugins].sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function runBeforeApplyLocalMutationsPlugins(args: {
|
|
848
|
+
plugins: readonly SyncClientPlugin[];
|
|
849
|
+
ctx: SyncClientPluginContext;
|
|
850
|
+
operations: SyncOperation[];
|
|
851
|
+
}): Promise<SyncOperation[]> {
|
|
852
|
+
if (args.plugins.length === 0) {
|
|
853
|
+
return args.operations;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
let transformedArgs: SyncClientLocalMutationArgs = cloneLocalMutationArgs({
|
|
857
|
+
operations: args.operations,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
for (const plugin of args.plugins) {
|
|
861
|
+
if (!plugin.beforeApplyLocalMutations) continue;
|
|
862
|
+
transformedArgs = await plugin.beforeApplyLocalMutations(
|
|
863
|
+
args.ctx,
|
|
864
|
+
transformedArgs
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return transformedArgs.operations;
|
|
869
|
+
}
|
|
870
|
+
|
|
735
871
|
export function createPushCommit<DB = AnyDb>(
|
|
736
872
|
config: PushCommitConfig
|
|
737
873
|
): MutationsCommitFn<DB, PushCommitMeta, undefined> {
|
|
@@ -915,10 +1051,7 @@ export function createPushCommit<DB = AnyDb>(
|
|
|
915
1051
|
};
|
|
916
1052
|
|
|
917
1053
|
const plugins = config.plugins ?? [];
|
|
918
|
-
|
|
919
|
-
const sortedPlugins = [...plugins].sort(
|
|
920
|
-
(a, b) => (a.priority ?? 50) - (b.priority ?? 50)
|
|
921
|
-
);
|
|
1054
|
+
const sortedPlugins = sortPlugins(plugins);
|
|
922
1055
|
const ctx: SyncClientPluginContext = {
|
|
923
1056
|
actorId: config.actorId ?? 'unknown',
|
|
924
1057
|
clientId: config.clientId,
|
package/src/plugins/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
SyncChange,
|
|
3
|
+
SyncOperation,
|
|
2
4
|
SyncPullRequest,
|
|
3
5
|
SyncPullResponse,
|
|
4
6
|
SyncPushRequest,
|
|
@@ -10,6 +12,22 @@ export interface SyncClientPluginContext {
|
|
|
10
12
|
clientId: string;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
export interface SyncClientWsDeliveryMetadata {
|
|
16
|
+
commitSeq?: number;
|
|
17
|
+
actorId?: string | null;
|
|
18
|
+
createdAt?: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SyncClientWsDeliveryArgs {
|
|
22
|
+
changes: SyncChange[];
|
|
23
|
+
cursor: number;
|
|
24
|
+
metadata?: SyncClientWsDeliveryMetadata;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SyncClientLocalMutationArgs {
|
|
28
|
+
operations: SyncOperation[];
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Plugin priority levels for ordering execution.
|
|
15
33
|
* Lower numbers execute first.
|
|
@@ -43,6 +61,16 @@ export interface SyncClientPlugin {
|
|
|
43
61
|
request: SyncPushRequest
|
|
44
62
|
): Promise<SyncPushRequest> | SyncPushRequest;
|
|
45
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Called before applying local optimistic mutations to the local DB.
|
|
66
|
+
* Use this when local payloads need shaping different from server push payloads
|
|
67
|
+
* (for example CRDT envelopes that should not be written as SQL columns).
|
|
68
|
+
*/
|
|
69
|
+
beforeApplyLocalMutations?(
|
|
70
|
+
ctx: SyncClientPluginContext,
|
|
71
|
+
args: SyncClientLocalMutationArgs
|
|
72
|
+
): Promise<SyncClientLocalMutationArgs> | SyncClientLocalMutationArgs;
|
|
73
|
+
|
|
46
74
|
/**
|
|
47
75
|
* Called after receiving a push response from the server.
|
|
48
76
|
* Receives both the request and the response to allow opIndex correlation.
|
|
@@ -60,4 +88,14 @@ export interface SyncClientPlugin {
|
|
|
60
88
|
ctx: SyncClientPluginContext,
|
|
61
89
|
args: { request: SyncPullRequest; response: SyncPullResponse }
|
|
62
90
|
): Promise<SyncPullResponse> | SyncPullResponse;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Called for inline WS-delivered changes before applying to the local DB.
|
|
94
|
+
* Use this when a plugin needs equivalent transforms for realtime payloads
|
|
95
|
+
* (for example decryption or CRDT materialization).
|
|
96
|
+
*/
|
|
97
|
+
beforeApplyWsChanges?(
|
|
98
|
+
ctx: SyncClientPluginContext,
|
|
99
|
+
args: SyncClientWsDeliveryArgs
|
|
100
|
+
): Promise<SyncClientWsDeliveryArgs> | SyncClientWsDeliveryArgs;
|
|
63
101
|
}
|