@syncular/server 0.0.6-126 → 0.0.6-135
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/dialect/base.d.ts +1 -0
- package/dist/dialect/base.d.ts.map +1 -1
- package/dist/dialect/base.js.map +1 -1
- package/dist/dialect/types.d.ts +6 -0
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/handlers/create-handler.d.ts +10 -0
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +161 -47
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/handlers/types.d.ts +24 -0
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +16 -5
- package/dist/notify.js.map +1 -1
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +93 -40
- package/dist/push.js.map +1 -1
- package/package.json +2 -2
- package/src/dialect/base.ts +1 -0
- package/src/dialect/types.ts +7 -0
- package/src/handlers/create-handler.ts +238 -66
- package/src/handlers/types.ts +30 -0
- package/src/notify.ts +15 -5
- package/src/push.ts +127 -56
package/src/handlers/types.ts
CHANGED
|
@@ -43,6 +43,11 @@ export interface ApplyOperationResult {
|
|
|
43
43
|
emittedChanges: EmittedChange[];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface BatchApplyOperationInput {
|
|
47
|
+
op: SyncOperation;
|
|
48
|
+
opIndex: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
/**
|
|
47
52
|
* Context for server operations.
|
|
48
53
|
*/
|
|
@@ -258,6 +263,18 @@ export interface ServerTableHandler<
|
|
|
258
263
|
*/
|
|
259
264
|
snapshotChunkTtlMs?: number;
|
|
260
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Hint for push engine savepoint optimization on single-op commits.
|
|
268
|
+
*
|
|
269
|
+
* When true, the handler guarantees that a rejected single operation
|
|
270
|
+
* does not leave durable writes behind before returning the rejection.
|
|
271
|
+
* This allows pushCommit() to skip SAVEPOINT overhead for single-op
|
|
272
|
+
* commits on savepoint-capable dialects.
|
|
273
|
+
*
|
|
274
|
+
* Omit or set false for custom handlers unless this guarantee holds.
|
|
275
|
+
*/
|
|
276
|
+
canRejectSingleOperationWithoutSavepoint?: boolean;
|
|
277
|
+
|
|
261
278
|
/**
|
|
262
279
|
* Resolve allowed scope values for the current actor.
|
|
263
280
|
*/
|
|
@@ -284,4 +301,17 @@ export interface ServerTableHandler<
|
|
|
284
301
|
op: SyncOperation,
|
|
285
302
|
opIndex: number
|
|
286
303
|
): Promise<ApplyOperationResult>;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Apply multiple operations in order.
|
|
307
|
+
*
|
|
308
|
+
* Implementations may internally batch writes but must preserve the same
|
|
309
|
+
* observable semantics as sequential applyOperation calls:
|
|
310
|
+
* - results are returned in operation order
|
|
311
|
+
* - processing stops at the first non-applied result
|
|
312
|
+
*/
|
|
313
|
+
applyOperationBatch?(
|
|
314
|
+
ctx: ServerApplyOperationContext<DB, Auth>,
|
|
315
|
+
operations: BatchApplyOperationInput[]
|
|
316
|
+
): Promise<ApplyOperationResult[]>;
|
|
287
317
|
}
|
package/src/notify.ts
CHANGED
|
@@ -106,12 +106,22 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
|
|
|
106
106
|
affected_tables: dialect.arrayToDb(tables) as string[],
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
let commitSeq = 0;
|
|
110
|
+
if (dialect.supportsInsertReturning) {
|
|
111
|
+
const insertedCommit = await syncTrx
|
|
112
|
+
.insertInto('sync_commits')
|
|
113
|
+
.values(commitRow)
|
|
114
|
+
.returning(['commit_seq'])
|
|
115
|
+
.executeTakeFirstOrThrow();
|
|
116
|
+
commitSeq = coerceNumber(insertedCommit.commit_seq) ?? 0;
|
|
117
|
+
} else {
|
|
118
|
+
const insertResult = await syncTrx
|
|
119
|
+
.insertInto('sync_commits')
|
|
120
|
+
.values(commitRow)
|
|
121
|
+
.executeTakeFirstOrThrow();
|
|
122
|
+
commitSeq = coerceNumber(insertResult.insertId) ?? 0;
|
|
123
|
+
}
|
|
113
124
|
|
|
114
|
-
let commitSeq = coerceNumber(insertResult.insertId) ?? 0;
|
|
115
125
|
if (commitSeq <= 0) {
|
|
116
126
|
// Fallback for dialects/drivers that don't provide insertId.
|
|
117
127
|
const inserted = await syncTrx
|
package/src/push.ts
CHANGED
|
@@ -306,20 +306,7 @@ export async function pushCommit<
|
|
|
306
306
|
result_json: null,
|
|
307
307
|
};
|
|
308
308
|
|
|
309
|
-
const
|
|
310
|
-
.insertInto('sync_commits')
|
|
311
|
-
.values(commitRow)
|
|
312
|
-
.onConflict((oc) =>
|
|
313
|
-
oc
|
|
314
|
-
.columns(['partition_id', 'client_id', 'client_commit_id'])
|
|
315
|
-
.doNothing()
|
|
316
|
-
)
|
|
317
|
-
.executeTakeFirstOrThrow();
|
|
318
|
-
|
|
319
|
-
const insertedRows = Number(
|
|
320
|
-
insertResult.numInsertedOrUpdatedRows ?? 0
|
|
321
|
-
);
|
|
322
|
-
if (insertedRows === 0) {
|
|
309
|
+
const loadExistingCommit = async (): Promise<PushCommitResult> => {
|
|
323
310
|
// Existing commit: return cached response (applied or rejected)
|
|
324
311
|
// Use forUpdate() for row locking on databases that support it
|
|
325
312
|
let query = (
|
|
@@ -393,11 +380,49 @@ export async function pushCommit<
|
|
|
393
380
|
scopeKeys: [],
|
|
394
381
|
emittedChanges: [],
|
|
395
382
|
};
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
let commitSeq = 0;
|
|
386
|
+
if (dialect.supportsInsertReturning) {
|
|
387
|
+
const insertedCommit = await syncTrx
|
|
388
|
+
.insertInto('sync_commits')
|
|
389
|
+
.values(commitRow)
|
|
390
|
+
.onConflict((oc) =>
|
|
391
|
+
oc
|
|
392
|
+
.columns(['partition_id', 'client_id', 'client_commit_id'])
|
|
393
|
+
.doNothing()
|
|
394
|
+
)
|
|
395
|
+
.returning(['commit_seq'])
|
|
396
|
+
.executeTakeFirst();
|
|
397
|
+
|
|
398
|
+
if (!insertedCommit) {
|
|
399
|
+
return loadExistingCommit();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
commitSeq = coerceNumber(insertedCommit.commit_seq) ?? 0;
|
|
403
|
+
} else {
|
|
404
|
+
const insertResult = await syncTrx
|
|
405
|
+
.insertInto('sync_commits')
|
|
406
|
+
.values(commitRow)
|
|
407
|
+
.onConflict((oc) =>
|
|
408
|
+
oc
|
|
409
|
+
.columns(['partition_id', 'client_id', 'client_commit_id'])
|
|
410
|
+
.doNothing()
|
|
411
|
+
)
|
|
412
|
+
.executeTakeFirstOrThrow();
|
|
413
|
+
|
|
414
|
+
const insertedRows = Number(
|
|
415
|
+
insertResult.numInsertedOrUpdatedRows ?? 0
|
|
416
|
+
);
|
|
417
|
+
if (insertedRows === 0) {
|
|
418
|
+
return loadExistingCommit();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
commitSeq = coerceNumber(insertResult.insertId) ?? 0;
|
|
396
422
|
}
|
|
397
423
|
|
|
398
|
-
let commitSeq = coerceNumber(insertResult.insertId) ?? 0;
|
|
399
424
|
if (commitSeq <= 0) {
|
|
400
|
-
const
|
|
425
|
+
const insertedCommitRow = await (
|
|
401
426
|
syncTrx.selectFrom('sync_commits') as SelectQueryBuilder<
|
|
402
427
|
SyncCoreDb,
|
|
403
428
|
'sync_commits',
|
|
@@ -409,12 +434,21 @@ export async function pushCommit<
|
|
|
409
434
|
.where('client_id', '=', request.clientId)
|
|
410
435
|
.where('client_commit_id', '=', request.clientCommitId)
|
|
411
436
|
.executeTakeFirstOrThrow();
|
|
412
|
-
commitSeq = Number(
|
|
437
|
+
commitSeq = Number(insertedCommitRow.commit_seq);
|
|
413
438
|
}
|
|
414
439
|
const commitId = `${request.clientId}:${request.clientCommitId}`;
|
|
415
440
|
|
|
416
441
|
const savepointName = 'sync_apply';
|
|
417
|
-
|
|
442
|
+
let useSavepoints = dialect.supportsSavepoints;
|
|
443
|
+
if (useSavepoints && ops.length === 1) {
|
|
444
|
+
const singleOpHandler = getServerHandlerOrThrow(
|
|
445
|
+
handlers,
|
|
446
|
+
ops[0]!.table
|
|
447
|
+
);
|
|
448
|
+
if (singleOpHandler.canRejectSingleOperationWithoutSavepoint) {
|
|
449
|
+
useSavepoints = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
418
452
|
let savepointCreated = false;
|
|
419
453
|
|
|
420
454
|
try {
|
|
@@ -429,44 +463,59 @@ export async function pushCommit<
|
|
|
429
463
|
const results = [];
|
|
430
464
|
const affectedTablesSet = new Set<string>();
|
|
431
465
|
|
|
432
|
-
for (let i = 0; i < ops.length;
|
|
466
|
+
for (let i = 0; i < ops.length; ) {
|
|
433
467
|
const op = ops[i]!;
|
|
434
468
|
const handler = getServerHandlerOrThrow(handlers, op.table);
|
|
435
|
-
const applied = await handler.applyOperation(
|
|
436
|
-
{
|
|
437
|
-
db: trx,
|
|
438
|
-
trx,
|
|
439
|
-
actorId,
|
|
440
|
-
auth: args.auth,
|
|
441
|
-
clientId: request.clientId,
|
|
442
|
-
commitId,
|
|
443
|
-
schemaVersion: request.schemaVersion,
|
|
444
|
-
},
|
|
445
|
-
op,
|
|
446
|
-
i
|
|
447
|
-
);
|
|
448
469
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
470
|
+
const operationCtx = {
|
|
471
|
+
db: trx,
|
|
472
|
+
trx,
|
|
473
|
+
actorId,
|
|
474
|
+
auth: args.auth,
|
|
475
|
+
clientId: request.clientId,
|
|
476
|
+
commitId,
|
|
477
|
+
schemaVersion: request.schemaVersion,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
let appliedBatch:
|
|
481
|
+
| Awaited<ReturnType<typeof handler.applyOperation>>[]
|
|
482
|
+
| null = null;
|
|
483
|
+
let consumed = 1;
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
handler.applyOperationBatch &&
|
|
487
|
+
dialect.supportsInsertReturning
|
|
488
|
+
) {
|
|
489
|
+
const batchInput = [];
|
|
490
|
+
for (let j = i; j < ops.length; j++) {
|
|
491
|
+
const nextOp = ops[j]!;
|
|
492
|
+
if (nextOp.table !== op.table) break;
|
|
493
|
+
batchInput.push({ op: nextOp, opIndex: j });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (batchInput.length > 1) {
|
|
497
|
+
appliedBatch = await handler.applyOperationBatch(
|
|
498
|
+
operationCtx,
|
|
499
|
+
batchInput
|
|
500
|
+
);
|
|
501
|
+
consumed = Math.max(1, appliedBatch.length);
|
|
502
|
+
}
|
|
457
503
|
}
|
|
458
504
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
505
|
+
if (!appliedBatch) {
|
|
506
|
+
appliedBatch = [
|
|
507
|
+
await handler.applyOperation(operationCtx, op, i),
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
if (appliedBatch.length === 0) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Handler "${op.table}" returned no results from applyOperationBatch`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (const applied of appliedBatch) {
|
|
517
|
+
if (applied.result.status !== 'applied') {
|
|
518
|
+
results.push(applied.result);
|
|
470
519
|
throw new RejectCommitError({
|
|
471
520
|
ok: true,
|
|
472
521
|
status: 'rejected',
|
|
@@ -474,13 +523,35 @@ export async function pushCommit<
|
|
|
474
523
|
results,
|
|
475
524
|
});
|
|
476
525
|
}
|
|
477
|
-
}
|
|
478
526
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
527
|
+
// Framework-level enforcement: emitted changes must have scopes
|
|
528
|
+
for (const c of applied.emittedChanges ?? []) {
|
|
529
|
+
const scopes = c?.scopes;
|
|
530
|
+
if (!scopes || typeof scopes !== 'object') {
|
|
531
|
+
results.push({
|
|
532
|
+
opIndex: applied.result.opIndex,
|
|
533
|
+
status: 'error' as const,
|
|
534
|
+
error: 'MISSING_SCOPES',
|
|
535
|
+
code: 'INVALID_SCOPE',
|
|
536
|
+
retriable: false,
|
|
537
|
+
});
|
|
538
|
+
throw new RejectCommitError({
|
|
539
|
+
ok: true,
|
|
540
|
+
status: 'rejected',
|
|
541
|
+
commitSeq,
|
|
542
|
+
results,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
results.push(applied.result);
|
|
548
|
+
allEmitted.push(...applied.emittedChanges);
|
|
549
|
+
for (const c of applied.emittedChanges) {
|
|
550
|
+
affectedTablesSet.add(c.table);
|
|
551
|
+
}
|
|
483
552
|
}
|
|
553
|
+
|
|
554
|
+
i += consumed;
|
|
484
555
|
}
|
|
485
556
|
|
|
486
557
|
if (allEmitted.length > 0) {
|