@abloatai/humans 0.59.2 → 0.61.0
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/README.md +1 -1
- package/dist/Ablo.d.ts +2 -10
- package/dist/Ablo.js +0 -1
- package/dist/client.d.ts +1 -48
- package/dist/humans.d.ts +1 -1
- package/dist/local/BaseSyncedStore.d.ts +5 -5
- package/dist/local/BaseSyncedStore.js +7 -7
- package/dist/local/Database.d.ts +2 -2
- package/dist/local/LazyReferenceCollection.d.ts +1 -1
- package/dist/local/Model.js +46 -56
- package/dist/local/NetworkMonitor.js +2 -0
- package/dist/local/RuntimeContext.js +2 -0
- package/dist/local/SyncClient.d.ts +12 -36
- package/dist/local/SyncClient.js +50 -108
- package/dist/local/client/createModelOperations.d.ts +3 -27
- package/dist/local/client/createModelOperations.js +20 -21
- package/dist/local/client/options.d.ts +14 -39
- package/dist/local/client/reactiveEngine.d.ts +3 -9
- package/dist/local/client/reactiveEngine.js +6 -151
- package/dist/local/client/storeLifecycle.js +5 -1
- package/dist/local/fileUploads.d.ts +27 -0
- package/dist/local/fileUploads.js +55 -0
- package/dist/local/storeContract.d.ts +5 -5
- package/dist/local/stores/syncAction.d.ts +4 -4
- package/dist/local/sync/contextOnChange.js +1 -1
- package/dist/local/sync/createClaimStream.js +1 -1
- package/dist/local/sync/credentialLifecycle.d.ts +4 -5
- package/dist/local/sync/credentialLifecycle.js +4 -5
- package/dist/local/sync/deltaPipeline.js +12 -6
- package/dist/local/sync/schemas.d.ts +10 -10
- package/dist/local/sync/scopeGroups.d.ts +11 -0
- package/dist/local/sync/scopeGroups.js +75 -0
- package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
- package/dist/local/transactions/localMutation.js +3 -3
- package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -5
- package/dist/local/transactions/mutations/MutationQueue.js +25 -51
- package/dist/local/transactions/mutations/batchProcessing.js +23 -10
- package/dist/local/transactions/mutations/commitPayload.d.ts +9 -2
- package/dist/local/transactions/mutations/commitTransport.js +3 -1
- package/dist/local/transactions/mutations/executionSelection.d.ts +0 -1
- package/dist/local/transactions/mutations/executionSelection.js +9 -17
- package/dist/local/transactions/mutations/failureHandling.js +9 -0
- package/dist/local/transactions/mutations/localMutation.js +3 -3
- package/dist/local/transactions/mutations/queueCoalescing.js +8 -0
- package/dist/local/transactions/mutations/replayValidation.d.ts +15 -15
- package/dist/react/AbloProvider.d.ts +11 -86
- package/dist/react/AbloProvider.js +11 -163
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/createAbloReact.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/surface.d.ts +3 -3
- package/dist/surface.js +1 -4
- package/package.json +3 -3
- package/src/Ablo.ts +5 -17
- package/src/client.ts +0 -51
- package/src/local/BaseSyncedStore.ts +14 -14
- package/src/local/Model.ts +45 -55
- package/src/local/NetworkMonitor.ts +2 -0
- package/src/local/RuntimeContext.ts +2 -0
- package/src/local/SyncClient.ts +73 -140
- package/src/local/client/createModelOperations.ts +30 -64
- package/src/local/client/options.ts +20 -43
- package/src/local/client/reactiveEngine.ts +7 -179
- package/src/local/client/storeLifecycle.ts +6 -1
- package/src/local/fileUploads.ts +97 -0
- package/src/local/storeContract.ts +5 -5
- package/src/local/sync/contextOnChange.ts +1 -1
- package/src/local/sync/createClaimStream.ts +1 -1
- package/src/local/sync/credentialLifecycle.ts +4 -5
- package/src/local/sync/deltaPipeline.ts +10 -6
- package/src/local/sync/scopeGroups.ts +91 -0
- package/src/local/sync/wsFrameHandlers.ts +0 -1
- package/src/local/transactions/localMutation.ts +3 -3
- package/src/local/transactions/mutations/MutationQueue.ts +24 -53
- package/src/local/transactions/mutations/batchProcessing.ts +25 -10
- package/src/local/transactions/mutations/commitPayload.ts +11 -1
- package/src/local/transactions/mutations/commitTransport.ts +2 -2
- package/src/local/transactions/mutations/executionSelection.ts +9 -15
- package/src/local/transactions/mutations/failureHandling.ts +10 -0
- package/src/local/transactions/mutations/localMutation.ts +3 -3
- package/src/local/transactions/mutations/queueCoalescing.ts +6 -0
- package/src/react/AbloProvider.tsx +17 -249
- package/src/react/useErrorListener.ts +1 -1
- package/src/react/useMutationFailureListener.ts +1 -1
- package/src/react.ts +1 -5
- package/src/surface.ts +1 -4
- package/dist/local/sync/participants.d.ts +0 -132
- package/dist/local/sync/participants.js +0 -342
- package/dist/local/transactions/mutations/pendingDrain.d.ts +0 -33
- package/dist/local/transactions/mutations/pendingDrain.js +0 -117
- package/src/local/sync/participants.ts +0 -564
- package/src/local/transactions/mutations/pendingDrain.ts +0 -169
|
@@ -111,6 +111,7 @@ export declare class MutationQueue extends EventEmitter {
|
|
|
111
111
|
private pendingPersistenceStages;
|
|
112
112
|
private persistenceStageScheduled;
|
|
113
113
|
private pendingDrainPromise;
|
|
114
|
+
private modelProcessingPromise;
|
|
114
115
|
private executionQueue;
|
|
115
116
|
private isProcessing;
|
|
116
117
|
private processTimer?;
|
|
@@ -153,7 +154,6 @@ export declare class MutationQueue extends EventEmitter {
|
|
|
153
154
|
private get failureHandlingContext();
|
|
154
155
|
private get conflictResolutionContext();
|
|
155
156
|
private get processingSchedulerContext();
|
|
156
|
-
private get pendingDrainContext();
|
|
157
157
|
private get durableCommitRestoreContext();
|
|
158
158
|
private get persistenceContext();
|
|
159
159
|
private nextCommitSequence;
|
|
@@ -225,7 +225,6 @@ export declare class MutationQueue extends EventEmitter {
|
|
|
225
225
|
*/
|
|
226
226
|
private scheduleReplicationLagTimeout;
|
|
227
227
|
private takeNextExecutionBatch;
|
|
228
|
-
private takePendingDrainBatch;
|
|
229
228
|
/**
|
|
230
229
|
* Resolvers for per-transaction `confirmation` promises. Populated in
|
|
231
230
|
* `attachConfirmation` at staging time, consumed by the constructor-time
|
|
@@ -477,7 +476,7 @@ export declare class MutationQueue extends EventEmitter {
|
|
|
477
476
|
awaitingDeltaCount: number;
|
|
478
477
|
awaitingDeltaTransactions: {
|
|
479
478
|
id: string;
|
|
480
|
-
type: "
|
|
479
|
+
type: "update" | "create" | "delete" | "archive" | "unarchive";
|
|
481
480
|
modelName: string;
|
|
482
481
|
modelId: string;
|
|
483
482
|
syncIdNeeded: number | undefined;
|
|
@@ -486,13 +485,13 @@ export declare class MutationQueue extends EventEmitter {
|
|
|
486
485
|
}[];
|
|
487
486
|
pendingTransactions: {
|
|
488
487
|
id: string;
|
|
489
|
-
type: "
|
|
488
|
+
type: "update" | "create" | "delete" | "archive" | "unarchive";
|
|
490
489
|
modelName: string;
|
|
491
490
|
modelId: string;
|
|
492
491
|
}[];
|
|
493
492
|
executingTransactions: {
|
|
494
493
|
id: string;
|
|
495
|
-
type: "
|
|
494
|
+
type: "update" | "create" | "delete" | "archive" | "unarchive";
|
|
496
495
|
modelName: string;
|
|
497
496
|
modelId: string;
|
|
498
497
|
}[];
|
|
@@ -34,9 +34,8 @@ import { enqueueTransaction } from './queueCoalescing.js';
|
|
|
34
34
|
import { processBatch } from './batchProcessing.js';
|
|
35
35
|
import { handleFailure } from './failureHandling.js';
|
|
36
36
|
import { handleConflict as resolveConflict, isPermanentError as classifyPermanentError, isDefinitiveRejection as classifyDefinitiveRejection } from './failurePolicy.js';
|
|
37
|
-
import { takeNextExecutionBatch as selectExecutionBatch
|
|
37
|
+
import { takeNextExecutionBatch as selectExecutionBatch } from './executionSelection.js';
|
|
38
38
|
import { scheduleProcessing as scheduleProcessingExternal } from './processingScheduler.js';
|
|
39
|
-
import { drainPendingConfirmations, } from './pendingDrain.js';
|
|
40
39
|
import { restoreDurableCommits as restoreDurableCommitsExternal } from './durableCommitRestore.js';
|
|
41
40
|
export class MutationQueue extends EventEmitter {
|
|
42
41
|
// Keep one hour of clock/network margin inside the server's 24-hour ledger.
|
|
@@ -70,6 +69,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
70
69
|
pendingPersistenceStages = [];
|
|
71
70
|
persistenceStageScheduled = false;
|
|
72
71
|
pendingDrainPromise = null;
|
|
72
|
+
modelProcessingPromise = null;
|
|
73
73
|
executionQueue = [];
|
|
74
74
|
isProcessing = false;
|
|
75
75
|
processTimer;
|
|
@@ -303,32 +303,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
303
303
|
logger: this.runtime.logger,
|
|
304
304
|
};
|
|
305
305
|
}
|
|
306
|
-
get pendingDrainContext() {
|
|
307
|
-
return {
|
|
308
|
-
runtime: this.runtime,
|
|
309
|
-
config: { deltaConfirmationTimeout: this.config.deltaConfirmationTimeout },
|
|
310
|
-
store: this.store,
|
|
311
|
-
executionQueue: this.executionQueue,
|
|
312
|
-
optimisticUpdates: this.localMutationPort.updates,
|
|
313
|
-
assertDurableReplayOpen: () => { this.assertDurableReplayOpen(); },
|
|
314
|
-
processCommitLane: () => this.processCommitLane(),
|
|
315
|
-
takePendingDrainBatch: (pending) => this.takePendingDrainBatch(pending),
|
|
316
|
-
ensureCommitEnvelope: (batch) => this.ensureCommitEnvelope(batch),
|
|
317
|
-
ensureDerivedFields: (transaction) => { this.ensureDerivedFields(transaction); },
|
|
318
|
-
sourceMutationIdsFor: (batch) => this.sourceMutationIdsFor(batch),
|
|
319
|
-
sealDurableCommit: (input) => this.sealDurableCommit(input),
|
|
320
|
-
assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
|
|
321
|
-
parseMutationCommitResult: (value) => this.parseMutationCommitResult(value),
|
|
322
|
-
dispatchCommitBounded: (...args) => this.dispatchCommitBounded(...args),
|
|
323
|
-
persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
|
|
324
|
-
removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
|
|
325
|
-
scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
|
|
326
|
-
scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => { this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs); },
|
|
327
|
-
enqueue: (transaction) => { this.enqueue(transaction); },
|
|
328
|
-
recentDeltaCorrelations: this.recentDeltaCorrelations,
|
|
329
|
-
emit: (event, payload) => this.emit(event, payload),
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
306
|
get durableCommitRestoreContext() {
|
|
333
307
|
return {
|
|
334
308
|
config: this.config,
|
|
@@ -738,9 +712,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
738
712
|
this.executionQueue = selected.remaining;
|
|
739
713
|
return selected.batch;
|
|
740
714
|
}
|
|
741
|
-
takePendingDrainBatch(pending) {
|
|
742
|
-
return selectPendingDrainBatch(pending, this.config.maxBatchSize);
|
|
743
|
-
}
|
|
744
715
|
/**
|
|
745
716
|
* Resolvers for per-transaction `confirmation` promises. Populated in
|
|
746
717
|
* `attachConfirmation` at staging time, consumed by the constructor-time
|
|
@@ -1004,25 +975,15 @@ export class MutationQueue extends EventEmitter {
|
|
|
1004
975
|
return this.pendingDrainPromise;
|
|
1005
976
|
}
|
|
1006
977
|
async drainPendingInternal() {
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
if (this.isProcessing)
|
|
1017
|
-
return;
|
|
1018
|
-
this.isProcessing = true;
|
|
1019
|
-
try {
|
|
1020
|
-
await drainPendingConfirmations(this.pendingDrainContext);
|
|
1021
|
-
}
|
|
1022
|
-
finally {
|
|
1023
|
-
this.isProcessing = false;
|
|
1024
|
-
if (this.executionQueue.length > 0)
|
|
1025
|
-
this.scheduleProcessing(true);
|
|
978
|
+
// Explicit flushes and reconnects are merely another trigger for the one
|
|
979
|
+
// model-mutation execution lane. A second sealing implementation can race
|
|
980
|
+
// the scheduled lane, consume its journal sources, and later dispatch the
|
|
981
|
+
// same transaction again. Move every staged row to the owned queue, then
|
|
982
|
+
// drive the normal lane until the queue has handed off all current work.
|
|
983
|
+
this.commitCreatedTransactions();
|
|
984
|
+
await this.processCommitLane();
|
|
985
|
+
while (this.executionQueue.length > 0 || this.modelProcessingPromise) {
|
|
986
|
+
await this.processBatch();
|
|
1026
987
|
}
|
|
1027
988
|
}
|
|
1028
989
|
async create(model, context, writeOptions, sourceMutationId) {
|
|
@@ -1055,7 +1016,20 @@ export class MutationQueue extends EventEmitter {
|
|
|
1055
1016
|
scheduleProcessingExternal(this.processingSchedulerContext, immediate);
|
|
1056
1017
|
}
|
|
1057
1018
|
async processBatch() {
|
|
1058
|
-
|
|
1019
|
+
if (this.modelProcessingPromise) {
|
|
1020
|
+
await this.modelProcessingPromise;
|
|
1021
|
+
if (this.executionQueue.length > 0)
|
|
1022
|
+
await this.processBatch();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const processing = processBatch(this.batchProcessingContext);
|
|
1026
|
+
const tracked = processing.finally(() => {
|
|
1027
|
+
if (this.modelProcessingPromise === tracked) {
|
|
1028
|
+
this.modelProcessingPromise = null;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
this.modelProcessingPromise = tracked;
|
|
1032
|
+
await tracked;
|
|
1059
1033
|
}
|
|
1060
1034
|
rememberDeltaCorrelation(correlationId, syncId) {
|
|
1061
1035
|
// Refresh insertion order when a replay repeats the same correlation id.
|
|
@@ -64,16 +64,29 @@ export async function processBatch(ctx) {
|
|
|
64
64
|
if (batchOps.length > 0) {
|
|
65
65
|
let dispatchStarted = false;
|
|
66
66
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
let durableEnvelope = batch[0]?.durableEnvelope;
|
|
68
|
+
if (durableEnvelope) {
|
|
69
|
+
const mismatched = batch.some((transaction) => transaction.durableEnvelope?.idempotencyKey !==
|
|
70
|
+
durableEnvelope?.idempotencyKey);
|
|
71
|
+
if (mismatched || durableEnvelope.idempotencyKey !== commitIdempotencyKey) {
|
|
72
|
+
throw new Error('Cannot replay a model batch with inconsistent durable envelopes');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
durableEnvelope = await ctx.sealDurableCommit({
|
|
77
|
+
idempotencyKey: commitIdempotencyKey,
|
|
78
|
+
origin: 'model_batch',
|
|
79
|
+
operations: batchOps.map(({ op }) => op),
|
|
80
|
+
sourceMutationIds: ctx.sourceMutationIdsFor(batch),
|
|
81
|
+
commitOptions: { reads: collectQueuedReads(batch) },
|
|
82
|
+
createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
|
|
83
|
+
sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
|
|
84
|
+
sequence: batch[0]?.commitEnvelope?.sequence,
|
|
85
|
+
});
|
|
86
|
+
for (const transaction of batch) {
|
|
87
|
+
transaction.durableEnvelope = durableEnvelope;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
77
90
|
const operations = durableEnvelope.operations;
|
|
78
91
|
// Capture lastSyncId from the server response for threshold-based
|
|
79
92
|
// confirmation.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
12
12
|
import { MutationOperationType } from '@abloatai/transaction/types';
|
|
13
13
|
import type { MutationOptions, WriteOptions } from '../../interfaces/index.js';
|
|
14
|
-
import type { CommitEnvelopeMember } from '@abloatai/transaction/commit';
|
|
14
|
+
import type { CommitEnvelopeMember, DurableCommitEnvelope } from '@abloatai/transaction/commit';
|
|
15
15
|
export interface UserContext {
|
|
16
16
|
userId: string;
|
|
17
17
|
organizationId: string;
|
|
@@ -69,6 +69,13 @@ export interface QueuedMutation {
|
|
|
69
69
|
* re-batching its operations under a fresh key.
|
|
70
70
|
*/
|
|
71
71
|
commitEnvelope?: CommitEnvelopeMember;
|
|
72
|
+
/**
|
|
73
|
+
* The exact durable request produced by the first successful local seal.
|
|
74
|
+
* Runtime retries dispatch this object directly. Asking the outbox to seal
|
|
75
|
+
* again is both unnecessary and unsafe after a concurrent authoritative
|
|
76
|
+
* completion has begun cleaning up the stored envelope.
|
|
77
|
+
*/
|
|
78
|
+
durableEnvelope?: DurableCommitEnvelope;
|
|
72
79
|
/** Pending-mutation journal entries atomically consumed by this envelope. */
|
|
73
80
|
sourceMutationIds?: string[];
|
|
74
81
|
/** Completed locally without a server operation; no sync echo will arrive. */
|
|
@@ -122,7 +129,7 @@ export declare const stripModelSuffix: (modelName: string) => string;
|
|
|
122
129
|
* foreign-key ordering because the row already exists, so they all share the
|
|
123
130
|
* configured default non-create priority.
|
|
124
131
|
*/
|
|
125
|
-
export declare const computePriorityScore: (type: QueuedMutation[
|
|
132
|
+
export declare const computePriorityScore: (type: QueuedMutation["type"], modelName: string, runtime?: RuntimeContext) => number;
|
|
126
133
|
export declare const TX_TYPE_TO_MUTATION_OP: Record<QueuedMutation['type'], MutationOperationType>;
|
|
127
134
|
export declare function hasStaleWriteOptions(options?: WriteOptions): boolean;
|
|
128
135
|
/** Options whose identity/audit semantics forbid merging two caller writes. */
|
|
@@ -95,7 +95,9 @@ export function dispatchCommitBounded(ctx, ...args) {
|
|
|
95
95
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
96
96
|
return dispatched;
|
|
97
97
|
return new Promise((resolve, reject) => {
|
|
98
|
-
const timer = setTimeout(() =>
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
reject(new AbloConnectionError('The mutation transport did not acknowledge the commit in time; its outcome remains pending and is safe to retry.', { code: 'commit_no_result' }));
|
|
100
|
+
}, timeoutMs);
|
|
99
101
|
dispatched.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error instanceof Error ? error : new Error(String(error))); });
|
|
100
102
|
});
|
|
101
103
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export function takeNextExecutionBatch(executionQueue, maxBatchSize) {
|
|
2
|
+
// Cancellation, delta confirmation, and failure settlement can all make a
|
|
3
|
+
// queued reference terminal before its scheduler callback runs. Terminal or
|
|
4
|
+
// currently executing rows have no authority to cross the dispatch boundary.
|
|
5
|
+
const pendingQueue = executionQueue.filter((tx) => tx.status === 'pending');
|
|
2
6
|
const retryGroups = new Map();
|
|
3
|
-
for (const tx of
|
|
7
|
+
for (const tx of pendingQueue) {
|
|
4
8
|
const envelope = tx.commitEnvelope;
|
|
5
9
|
if (!envelope)
|
|
6
10
|
continue;
|
|
@@ -13,30 +17,18 @@ export function takeNextExecutionBatch(executionQueue, maxBatchSize) {
|
|
|
13
17
|
const expectedCount = members[0]?.commitEnvelope?.operationCount;
|
|
14
18
|
if (expectedCount === undefined || members.length !== expectedCount)
|
|
15
19
|
continue;
|
|
16
|
-
const remaining =
|
|
20
|
+
const remaining = pendingQueue.filter((tx) => tx.commitEnvelope?.idempotencyKey !== idempotencyKey);
|
|
17
21
|
members.sort((a, b) => (a.commitEnvelope?.operationIndex ?? 0) - (b.commitEnvelope?.operationIndex ?? 0));
|
|
18
22
|
return { batch: members, remaining };
|
|
19
23
|
}
|
|
20
|
-
const fresh =
|
|
24
|
+
const fresh = pendingQueue.filter((tx) => !tx.commitEnvelope);
|
|
21
25
|
const firstFresh = fresh[0];
|
|
22
26
|
if (!firstFresh)
|
|
23
|
-
return { batch: [], remaining:
|
|
27
|
+
return { batch: [], remaining: pendingQueue };
|
|
24
28
|
const explicitIndex = fresh.findIndex((tx) => typeof tx.writeOptions?.idempotencyKey === 'string');
|
|
25
29
|
const selected = explicitIndex === 0
|
|
26
30
|
? [firstFresh]
|
|
27
31
|
: fresh.slice(0, Math.min(maxBatchSize, explicitIndex > 0 ? explicitIndex : fresh.length));
|
|
28
32
|
const selectedIds = new Set(selected.map((tx) => tx.id));
|
|
29
|
-
return { batch: selected, remaining:
|
|
30
|
-
}
|
|
31
|
-
export function takePendingDrainBatch(pending, maxBatchSize) {
|
|
32
|
-
const first = pending[0];
|
|
33
|
-
if (!first)
|
|
34
|
-
return [];
|
|
35
|
-
const envelope = first.commitEnvelope;
|
|
36
|
-
if (envelope)
|
|
37
|
-
return pending.filter((tx) => tx.commitEnvelope?.idempotencyKey === envelope.idempotencyKey);
|
|
38
|
-
if (typeof first.writeOptions?.idempotencyKey === 'string')
|
|
39
|
-
return [first];
|
|
40
|
-
const explicitIndex = pending.findIndex((tx) => typeof tx.writeOptions?.idempotencyKey === 'string');
|
|
41
|
-
return pending.slice(0, Math.min(maxBatchSize, explicitIndex > 0 ? explicitIndex : pending.length));
|
|
33
|
+
return { batch: selected, remaining: pendingQueue.filter((tx) => !selectedIds.has(tx.id)) };
|
|
42
34
|
}
|
|
@@ -13,6 +13,15 @@ export function transientRetryDelayMs(error, attempt, retryBackoff) {
|
|
|
13
13
|
return Math.floor(Math.random() * ceiling);
|
|
14
14
|
}
|
|
15
15
|
export async function handleFailure(ctx, transaction, error) {
|
|
16
|
+
// The dispatch owner may lose its acknowledgement while an authoritative
|
|
17
|
+
// delta concurrently completes the same transaction. Completion is
|
|
18
|
+
// terminal: a late catch path must not turn that row back into `pending`
|
|
19
|
+
// and schedule a second seal after its durable sources were cleaned up.
|
|
20
|
+
if (transaction.status === 'completed' ||
|
|
21
|
+
transaction.status === 'failed' ||
|
|
22
|
+
transaction.status === 'rolled_back' ||
|
|
23
|
+
transaction.status === 'awaiting_delta')
|
|
24
|
+
return;
|
|
16
25
|
transaction.attempts++;
|
|
17
26
|
// Check whether this is a permanent error that should not be retried.
|
|
18
27
|
if (ctx.isPermanentError(error)) {
|
|
@@ -12,9 +12,9 @@ export function createLocalMutationPort(emitter) {
|
|
|
12
12
|
const updates = new Map();
|
|
13
13
|
return {
|
|
14
14
|
updates,
|
|
15
|
-
applyCreate: (model, transaction) => applyOptimisticCreate(updates, emitter, model, transaction),
|
|
16
|
-
applyUpdate: (model, transaction) => applyOptimisticUpdate(updates, emitter, model, transaction),
|
|
17
|
-
applyDelete: (model, transaction) => applyOptimisticDelete(updates, emitter, model, transaction),
|
|
15
|
+
applyCreate: (model, transaction) => { applyOptimisticCreate(updates, emitter, model, transaction); },
|
|
16
|
+
applyUpdate: (model, transaction) => { applyOptimisticUpdate(updates, emitter, model, transaction); },
|
|
17
|
+
applyDelete: (model, transaction) => { applyOptimisticDelete(updates, emitter, model, transaction); },
|
|
18
18
|
rollback: (transaction, reason, error) => rollbackOptimistic(updates, emitter, transaction, reason, error),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { mergeUpdateData } from './coalesceRules.js';
|
|
2
2
|
import { hasCommitCoalescingBarrier } from './commitPayload.js';
|
|
3
3
|
export function enqueueTransaction(ctx, transaction) {
|
|
4
|
+
// Only the pending state may cross into the execution owner. A late timer,
|
|
5
|
+
// reconnect callback, or stale staging callback must not resurrect a row
|
|
6
|
+
// that is already executing or terminal, and repeated triggers must not put
|
|
7
|
+
// the same source mutation into the queue twice.
|
|
8
|
+
if (transaction.status !== 'pending')
|
|
9
|
+
return;
|
|
10
|
+
if (ctx.executionQueue.some((candidate) => candidate.id === transaction.id))
|
|
11
|
+
return;
|
|
4
12
|
ctx.ensureDerivedFields(transaction);
|
|
5
13
|
const modelKey = `${transaction.modelName}:${transaction.modelId}`;
|
|
6
14
|
if (transaction.type === 'update' && transaction.attempts === 0 && !transaction.commitEnvelope) {
|
|
@@ -27,11 +27,11 @@ import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
|
27
27
|
export declare const persistedTransactionSchema: z.ZodObject<{
|
|
28
28
|
id: z.ZodString;
|
|
29
29
|
type: z.ZodEnum<{
|
|
30
|
-
|
|
30
|
+
update: "update";
|
|
31
31
|
create: "create";
|
|
32
32
|
delete: "delete";
|
|
33
|
+
archive: "archive";
|
|
33
34
|
unarchive: "unarchive";
|
|
34
|
-
update: "update";
|
|
35
35
|
}>;
|
|
36
36
|
modelName: z.ZodString;
|
|
37
37
|
modelId: z.ZodString;
|
|
@@ -95,10 +95,10 @@ export declare function deserializePersistedTransaction(row: unknown, runtime?:
|
|
|
95
95
|
export declare const persistedMutationSchema: z.ZodObject<{
|
|
96
96
|
mutationId: z.ZodOptional<z.ZodString>;
|
|
97
97
|
type: z.ZodEnum<{
|
|
98
|
-
|
|
98
|
+
update: "update";
|
|
99
99
|
create: "create";
|
|
100
100
|
delete: "delete";
|
|
101
|
-
|
|
101
|
+
archive: "archive";
|
|
102
102
|
}>;
|
|
103
103
|
modelData: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
104
104
|
modelName: z.ZodString;
|
|
@@ -133,14 +133,15 @@ export declare const PENDING_MUTATION_RECORD_PREFIX = "pending-mutation:";
|
|
|
133
133
|
export declare const PENDING_MUTATION_REPLAY_WINDOW_MS: number;
|
|
134
134
|
/** Scope-less records written by the first aggregate-journal release. */
|
|
135
135
|
export declare const legacyPendingMutationRecordSchema: z.ZodObject<{
|
|
136
|
+
storageVersion: z.ZodLiteral<1>;
|
|
136
137
|
id: z.ZodString;
|
|
137
138
|
type: z.ZodLiteral<"pending_mutation">;
|
|
138
139
|
mutation: z.ZodObject<{
|
|
139
140
|
type: z.ZodEnum<{
|
|
140
|
-
|
|
141
|
+
update: "update";
|
|
141
142
|
create: "create";
|
|
142
143
|
delete: "delete";
|
|
143
|
-
|
|
144
|
+
archive: "archive";
|
|
144
145
|
}>;
|
|
145
146
|
modelData: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
146
147
|
modelName: z.ZodString;
|
|
@@ -167,17 +168,22 @@ export declare const legacyPendingMutationRecordSchema: z.ZodObject<{
|
|
|
167
168
|
mutationId: z.ZodString;
|
|
168
169
|
}, z.core.$loose>;
|
|
169
170
|
timestamp: z.ZodNumber;
|
|
170
|
-
storageVersion: z.ZodLiteral<1>;
|
|
171
171
|
}, z.core.$strict>;
|
|
172
172
|
export declare const pendingMutationRecordSchema: z.ZodObject<{
|
|
173
|
+
storageVersion: z.ZodLiteral<2>;
|
|
174
|
+
scope: z.ZodObject<{
|
|
175
|
+
organizationId: z.ZodString;
|
|
176
|
+
participantId: z.ZodString;
|
|
177
|
+
namespace: z.ZodString;
|
|
178
|
+
}, z.core.$strict>;
|
|
173
179
|
id: z.ZodString;
|
|
174
180
|
type: z.ZodLiteral<"pending_mutation">;
|
|
175
181
|
mutation: z.ZodObject<{
|
|
176
182
|
type: z.ZodEnum<{
|
|
177
|
-
|
|
183
|
+
update: "update";
|
|
178
184
|
create: "create";
|
|
179
185
|
delete: "delete";
|
|
180
|
-
|
|
186
|
+
archive: "archive";
|
|
181
187
|
}>;
|
|
182
188
|
modelData: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
183
189
|
modelName: z.ZodString;
|
|
@@ -204,12 +210,6 @@ export declare const pendingMutationRecordSchema: z.ZodObject<{
|
|
|
204
210
|
mutationId: z.ZodString;
|
|
205
211
|
}, z.core.$loose>;
|
|
206
212
|
timestamp: z.ZodNumber;
|
|
207
|
-
storageVersion: z.ZodLiteral<2>;
|
|
208
|
-
scope: z.ZodObject<{
|
|
209
|
-
organizationId: z.ZodString;
|
|
210
|
-
participantId: z.ZodString;
|
|
211
|
-
namespace: z.ZodString;
|
|
212
|
-
}, z.core.$strict>;
|
|
213
213
|
}, z.core.$strict>;
|
|
214
214
|
export type PendingMutationRecord = z.infer<typeof pendingMutationRecordSchema>;
|
|
215
215
|
export declare function pendingMutationRecordId(mutationId: string): string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
3
3
|
import type { AbloClient as Ablo } from '../client.js';
|
|
4
|
-
import type {
|
|
5
|
-
import type {
|
|
4
|
+
import type { Peer } from '@abloatai/transaction/types/streams';
|
|
5
|
+
import type { GroupScope } from '../local/sync/scopeGroups.js';
|
|
6
6
|
import { type SyncStoreContract } from './context.js';
|
|
7
7
|
/**
|
|
8
8
|
* Ablo umbrella provider — owns the sync engine, multiplayer, and
|
|
@@ -13,9 +13,8 @@ import { type SyncStoreContract } from './context.js';
|
|
|
13
13
|
*
|
|
14
14
|
* - **One component, one import.** Consumers write the provider
|
|
15
15
|
* once at the root; nothing else needs to plumb the engine.
|
|
16
|
-
* - **Multiplayer is default.** React consumers
|
|
17
|
-
*
|
|
18
|
-
* available. No opt-in prop.
|
|
16
|
+
* - **Multiplayer is default.** React consumers share the client's scoped
|
|
17
|
+
* groups, presence stream, and model surface without another join step.
|
|
19
18
|
* - **Declarative props for app glue.** `preventUnsavedChanges`,
|
|
20
19
|
* `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
|
|
21
20
|
* absorbs a class of integration code that previously lived in
|
|
@@ -35,7 +34,7 @@ import { type SyncStoreContract } from './context.js';
|
|
|
35
34
|
* // Build once at module scope — a new instance per render tears down the socket.
|
|
36
35
|
* // The endpoint string points at your session-mint route (`ablo init`
|
|
37
36
|
* // scaffolds it); the SDK fetches it and keeps the token fresh.
|
|
38
|
-
* const ablo = Ablo({ schema,
|
|
37
|
+
* const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
|
|
39
38
|
*
|
|
40
39
|
* <AbloProvider client={ablo}>
|
|
41
40
|
* <App />
|
|
@@ -108,82 +107,11 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
|
108
107
|
children: ReactNode;
|
|
109
108
|
}
|
|
110
109
|
export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProviderProps<R>): React.ReactElement;
|
|
111
|
-
export type {
|
|
112
|
-
/**
|
|
113
|
-
* Options for `useJoin`. The hook reuses the engine's single
|
|
114
|
-
* WebSocket and opens a scoped claim on it when `scope` is provided:
|
|
115
|
-
* one TCP connection, N logical sub-syncgroup participants.
|
|
116
|
-
*/
|
|
117
|
-
export interface UseJoinOptions {
|
|
118
|
-
readonly scope?: ParticipantScope;
|
|
119
|
-
/**
|
|
120
|
-
* Lease TTL for the participant claim, as a compact duration (`'5m'`) or a
|
|
121
|
-
* number of seconds. The same dial and the same spelling as
|
|
122
|
-
* `ablo.<model>.join(ids, { ttl })` and every other lease in the SDK.
|
|
123
|
-
*/
|
|
124
|
-
readonly ttl?: Duration;
|
|
125
|
-
/**
|
|
126
|
-
* @deprecated Use `ttl`. Removed in 0.37.0.
|
|
127
|
-
*
|
|
128
|
-
* The same rename as on `ParticipantJoinOptions`: one lease, and the seconds
|
|
129
|
-
* spelling was the one that misled, because it accepted duration strings too.
|
|
130
|
-
*/
|
|
131
|
-
readonly ttlSeconds?: number | string | null;
|
|
132
|
-
/** Tear down + don't re-join while true. */
|
|
133
|
-
readonly paused?: boolean;
|
|
134
|
-
/**
|
|
135
|
-
* Acquire a write-claim CLAIM on the scope, in addition to read interest.
|
|
136
|
-
*
|
|
137
|
-
* Default `false`: opening a scope subscribes the connection to its deltas
|
|
138
|
-
* (read interest, via `update_subscription`) but does NOT claim it — a
|
|
139
|
-
* viewer is not a claimant. Set `true` when the participant intends to
|
|
140
|
-
* WRITE (editing a report, an agent staking work): the claim is sent so peers
|
|
141
|
-
* observe it, and the scope is pinned so it stays subscribed (never warms)
|
|
142
|
-
* for as long as the claim is held.
|
|
143
|
-
*/
|
|
144
|
-
readonly claim?: boolean;
|
|
145
|
-
/**
|
|
146
|
-
* Backfill the scope's CURRENT state into the pool on enter, in addition to
|
|
147
|
-
* tailing live changes.
|
|
148
|
-
*
|
|
149
|
-
* Default `false`: entering a scope subscribes to its FUTURE deltas only — if
|
|
150
|
-
* the scope's rows aren't already loaded, the view is empty until something
|
|
151
|
-
* changes. Set `true` when opening an entity that may not be loaded yet (a
|
|
152
|
-
* deep-linked report, a never-opened ledger) so its current rows are fetched and
|
|
153
|
-
* injected once, then kept fresh by the live tail. The fetch is single-flight
|
|
154
|
-
* and runs once per group; a failure soft-fails (the live tail still flows).
|
|
155
|
-
*/
|
|
156
|
-
readonly hydrate?: boolean;
|
|
157
|
-
}
|
|
158
|
-
export interface UseJoinReturn {
|
|
159
|
-
readonly participant: EngineParticipant | null;
|
|
160
|
-
/** Everyone else on the engine's sync groups (`participant.presence.others`), bridged to React. */
|
|
161
|
-
readonly peers: readonly Peer[];
|
|
162
|
-
/** Active claim claims by peers (`participant.claims.others`), bridged to React. */
|
|
163
|
-
readonly claims: readonly Claim[];
|
|
164
|
-
readonly status: ParticipantStatus;
|
|
165
|
-
readonly error: Error | null;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Join multiplayer for a given scope. Returns the participant and its
|
|
169
|
-
* lifecycle status. Auto-cleans up on unmount or when `paused`
|
|
170
|
-
* flips to true.
|
|
171
|
-
*
|
|
172
|
-
* `useJoin` is the React form of `ablo.<model>.join` — scope-level
|
|
173
|
-
* read-interest + presence; returns the reactive participant facade
|
|
174
|
-
* (peers/claims/status).
|
|
175
|
-
*
|
|
176
|
-
* The returned `participant` is an `EngineParticipant` — `.presence`
|
|
177
|
-
* + `.claims` only — backed by the engine's existing socket. For
|
|
178
|
-
* headless-bot patterns (a separate identity in the same browser
|
|
179
|
-
* tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
|
|
180
|
-
*/
|
|
181
|
-
export declare function useJoin(opts: UseJoinOptions): UseJoinReturn;
|
|
110
|
+
export type { GroupScope };
|
|
182
111
|
/**
|
|
183
112
|
* Read-only presence: the OTHER participants currently visible to this
|
|
184
|
-
* connection, bridged to React.
|
|
185
|
-
*
|
|
186
|
-
* it is a pure reader of the engine's already-flowing presence stream.
|
|
113
|
+
* connection, bridged to React. This is a pure reader of the engine's
|
|
114
|
+
* already-flowing presence stream; it does not mutate connection groups.
|
|
187
115
|
*
|
|
188
116
|
* Pass `scope` to narrow to the peers on that scope's sync group(s); omit
|
|
189
117
|
* it to get everyone on the engine's groups. Membership is driven entirely
|
|
@@ -191,18 +119,15 @@ export declare function useJoin(opts: UseJoinOptions): UseJoinReturn;
|
|
|
191
119
|
* cursor/collaboration traffic), so reading it never affects what the
|
|
192
120
|
* connection is subscribed to and can't deadlock against a gated channel.
|
|
193
121
|
*
|
|
194
|
-
* Use this to answer "is anyone else here?"
|
|
195
|
-
* broadcasts while alone
|
|
196
|
-
* read interest (scope `leave` is not reference-counted, so a second
|
|
197
|
-
* `useJoin` on the same scope would warm-drop the owner's
|
|
198
|
-
* subscription on unmount).
|
|
122
|
+
* Use this to answer "is anyone else here?", for example to suppress
|
|
123
|
+
* live-cursor broadcasts while alone.
|
|
199
124
|
*
|
|
200
125
|
* ```ts
|
|
201
126
|
* const peers = usePeers({ reports: reportId });
|
|
202
127
|
* const alone = !peers.some((p) => p.participantKind === 'user');
|
|
203
128
|
* ```
|
|
204
129
|
*/
|
|
205
|
-
export declare function usePeers(scope?:
|
|
130
|
+
export declare function usePeers(scope?: GroupScope): readonly Peer[];
|
|
206
131
|
/**
|
|
207
132
|
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
208
133
|
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|