@abloatai/humans 0.57.0 → 0.58.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/dist/Ablo.d.ts +8 -18
- package/dist/client.d.ts +6 -19
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/local/Database.d.ts +1 -1
- package/dist/local/SyncClient.js +5 -0
- package/dist/local/client/createInternalComponents.d.ts +1 -1
- package/dist/local/client/createInternalComponents.js +1 -1
- package/dist/local/client/{createModelProxy.d.ts → createModelOperations.d.ts} +11 -10
- package/dist/local/client/{createModelProxy.js → createModelOperations.js} +240 -313
- package/dist/local/client/options.d.ts +2 -2
- package/dist/local/client/reactiveEngine.js +34 -45
- package/dist/local/client/resourceTypes.d.ts +3 -3
- package/dist/local/client/resourceTypes.js +1 -1
- package/dist/local/client/wsMutationExecutor.d.ts +4 -4
- package/dist/local/client/wsMutationExecutor.js +3 -5
- package/dist/local/coordination/ClaimLog.d.ts +3 -3
- package/dist/local/coordination/ClaimLog.js +2 -2
- package/dist/local/interfaces/index.d.ts +5 -7
- package/dist/local/mutators/mutateActions.d.ts +3 -2
- package/dist/local/query/types.d.ts +2 -2
- package/dist/local/stores/syncAction.d.ts +1 -1
- package/dist/local/stores/syncAction.js +1 -1
- package/dist/local/sync/ConnectionManager.d.ts +1 -1
- package/dist/local/sync/ConnectionManager.js +1 -1
- package/dist/local/sync/OnDemandLoader.d.ts +1 -1
- package/dist/local/sync/OnDemandLoader.js +1 -1
- package/dist/local/sync/SyncWebSocket.d.ts +3 -3
- package/dist/local/sync/SyncWebSocket.js +2 -2
- package/dist/local/sync/commitFrames.d.ts +1 -1
- package/dist/local/sync/commitFrames.js +1 -1
- package/dist/local/sync/contextOnChange.d.ts +8 -0
- package/dist/local/sync/contextOnChange.js +65 -0
- package/dist/local/sync/createClaimStream.d.ts +1 -1
- package/dist/local/sync/createClaimStream.js +47 -28
- package/dist/local/sync/credentialLifecycle.d.ts +1 -1
- package/dist/local/sync/credentialLifecycle.js +1 -1
- package/dist/local/sync/participants.d.ts +1 -1
- package/dist/local/sync/schemas.d.ts +2 -2
- package/dist/local/sync/schemas.js +1 -1
- package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
- package/dist/local/sync/wsFrameHandlers.js +1 -1
- package/dist/local/transactions/databaseCommitOutbox.d.ts +2 -2
- package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -19
- package/dist/local/transactions/mutations/MutationQueue.js +5 -70
- package/dist/local/transactions/mutations/batchProcessing.d.ts +2 -13
- package/dist/local/transactions/mutations/batchProcessing.js +0 -20
- package/dist/local/transactions/mutations/commitApi.d.ts +2 -3
- package/dist/local/transactions/mutations/commitApi.js +0 -4
- package/dist/local/transactions/mutations/commitLane.d.ts +3 -8
- package/dist/local/transactions/mutations/commitLane.js +0 -11
- package/dist/local/transactions/mutations/commitPayload.d.ts +2 -4
- package/dist/local/transactions/mutations/commitPayload.js +2 -6
- package/dist/local/transactions/mutations/commitTransport.d.ts +3 -4
- package/dist/local/transactions/mutations/commitTransport.js +2 -5
- package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
- package/dist/local/transactions/mutations/durableCommitRestore.js +1 -4
- package/dist/local/transactions/mutations/durableWriteStore.d.ts +5 -5
- package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
- package/dist/local/transactions/mutations/failurePolicy.d.ts +2 -2
- package/dist/local/transactions/mutations/mutationPersistence.d.ts +1 -1
- package/dist/local/transactions/mutations/pendingDrain.d.ts +2 -2
- package/dist/local/transactions/mutations/replayValidation.d.ts +0 -20
- package/dist/local/transactions/mutations/replayValidation.js +1 -3
- package/dist/local/transactions/persistedTransaction.d.ts +0 -2
- package/dist/plugin.d.ts +2 -2
- package/dist/presenceStream.d.ts +1 -1
- package/dist/react/createAbloReact.d.ts +1 -1
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useAblo.js +1 -1
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -2
- package/package.json +2 -2
- package/src/Ablo.ts +9 -27
- package/src/client.ts +6 -22
- package/src/core.ts +1 -1
- package/src/local/Database.ts +1 -1
- package/src/local/SyncClient.ts +5 -0
- package/src/local/client/createInternalComponents.ts +2 -2
- package/src/local/client/{createModelProxy.ts → createModelOperations.ts} +292 -367
- package/src/local/client/options.ts +2 -2
- package/src/local/client/reactiveEngine.ts +63 -55
- package/src/local/client/resourceTypes.ts +4 -4
- package/src/local/client/wsMutationExecutor.ts +3 -9
- package/src/local/coordination/ClaimLog.ts +3 -3
- package/src/local/interfaces/index.ts +5 -7
- package/src/local/mutators/mutateActions.ts +3 -2
- package/src/local/query/types.ts +2 -2
- package/src/local/stores/syncAction.ts +1 -1
- package/src/local/sync/ConnectionManager.ts +1 -1
- package/src/local/sync/OnDemandLoader.ts +1 -1
- package/src/local/sync/SyncWebSocket.ts +3 -3
- package/src/local/sync/commitFrames.ts +1 -2
- package/src/local/sync/contextOnChange.ts +92 -0
- package/src/local/sync/createClaimStream.ts +77 -39
- package/src/local/sync/credentialLifecycle.ts +1 -1
- package/src/local/sync/participants.ts +1 -1
- package/src/local/sync/schemas.ts +1 -1
- package/src/local/sync/wsFrameHandlers.ts +1 -1
- package/src/local/transactions/databaseCommitOutbox.ts +2 -2
- package/src/local/transactions/mutations/MutationQueue.ts +8 -95
- package/src/local/transactions/mutations/batchProcessing.ts +2 -38
- package/src/local/transactions/mutations/commitApi.ts +3 -7
- package/src/local/transactions/mutations/commitLane.ts +3 -17
- package/src/local/transactions/mutations/commitPayload.ts +3 -11
- package/src/local/transactions/mutations/commitTransport.ts +3 -7
- package/src/local/transactions/mutations/durableCommitRestore.ts +2 -5
- package/src/local/transactions/mutations/durableWriteStore.ts +5 -5
- package/src/local/transactions/mutations/failurePolicy.ts +2 -2
- package/src/local/transactions/mutations/mutationPersistence.ts +1 -1
- package/src/local/transactions/mutations/pendingDrain.ts +2 -2
- package/src/local/transactions/mutations/replayValidation.ts +1 -3
- package/src/local/transactions/persistedTransaction.ts +0 -3
- package/src/plugin.ts +2 -2
- package/src/presenceStream.ts +2 -2
- package/src/react/createAbloReact.ts +1 -1
- package/src/react/useAblo.ts +2 -2
- package/src/surface.ts +2 -3
- package/dist/local/sync/createSnapshot.d.ts +0 -29
- package/dist/local/sync/createSnapshot.js +0 -116
- package/src/local/sync/createSnapshot.ts +0 -160
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import type {
|
|
26
26
|
WsTransport,
|
|
27
27
|
PresenceUpdate,
|
|
28
|
-
} from '@abloatai/transaction/transport/
|
|
28
|
+
} from '@abloatai/transaction/transport/websocket';
|
|
29
29
|
import type {
|
|
30
30
|
ClaimOptions,
|
|
31
31
|
ClaimTarget,
|
|
@@ -52,10 +52,15 @@ import {
|
|
|
52
52
|
streamTarget,
|
|
53
53
|
wireTarget,
|
|
54
54
|
type ClaimTargetDetails,
|
|
55
|
-
} from '@abloatai/transaction/
|
|
56
|
-
import {
|
|
55
|
+
} from '@abloatai/transaction/claims';
|
|
56
|
+
import {
|
|
57
|
+
bindClaimLifetime,
|
|
58
|
+
createClaimLifetime,
|
|
59
|
+
type ClaimLifetime,
|
|
60
|
+
} from '@abloatai/transaction/claims/lifetime';
|
|
61
|
+
import { declaredMeta } from '@abloatai/transaction/claims';
|
|
57
62
|
import { AbloClaimedError, AbloConnectionError } from '@abloatai/transaction/errors';
|
|
58
|
-
import { resolveHeartbeatOptions } from '@abloatai/transaction/
|
|
63
|
+
import { resolveHeartbeatOptions } from '@abloatai/transaction/claims';
|
|
59
64
|
import { noopLogger, type Logger } from '@abloatai/transaction/logger';
|
|
60
65
|
|
|
61
66
|
/**
|
|
@@ -114,13 +119,11 @@ export interface AttachableClaimStream extends ClaimStream {
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
/**
|
|
117
|
-
* What this participant needs to remember about
|
|
118
|
-
* re-announce it after a reconnect.
|
|
122
|
+
* What this participant needs to remember about one open grant.
|
|
119
123
|
*
|
|
120
124
|
* The sub-entity locator is taken from {@link ClaimTargetDetails} rather than
|
|
121
|
-
* listed again: a member this record forgot would be a member
|
|
122
|
-
*
|
|
123
|
-
* believes it has.
|
|
125
|
+
* listed again: a member this record forgot would be a member release or loss
|
|
126
|
+
* bookkeeping silently narrowed relative to the handle the caller holds.
|
|
124
127
|
*/
|
|
125
128
|
type OwnClaim = ClaimTargetDetails & {
|
|
126
129
|
readonly entityType: string;
|
|
@@ -133,6 +136,11 @@ type OwnClaim = ClaimTargetDetails & {
|
|
|
133
136
|
readonly queue?: boolean;
|
|
134
137
|
};
|
|
135
138
|
|
|
139
|
+
interface OwnGrant {
|
|
140
|
+
readonly claim: OwnClaim;
|
|
141
|
+
readonly lifetime: ClaimLifetime;
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
export function createClaimStream(
|
|
137
145
|
config: ClaimStreamConfig,
|
|
138
146
|
transport: ClaimTransport | null = null,
|
|
@@ -146,8 +154,8 @@ export function createClaimStream(
|
|
|
146
154
|
const activeByClaimId = new Map<string, Claim>();
|
|
147
155
|
let claimsSnapshot: readonly Claim[] = Object.freeze([]);
|
|
148
156
|
|
|
149
|
-
// ── State: our own open
|
|
150
|
-
const ownClaims = new Map<string,
|
|
157
|
+
// ── State: our own open grants, keyed by the exact claim id ─────
|
|
158
|
+
const ownClaims = new Map<string, OwnGrant>();
|
|
151
159
|
|
|
152
160
|
// ── State: per-entity wait queues, from `claim_queue` frames ────
|
|
153
161
|
// Keyed `type:id`; the value is the FIFO line of queued claims. Powers
|
|
@@ -290,7 +298,8 @@ export function createClaimStream(
|
|
|
290
298
|
unsubs.push(
|
|
291
299
|
t.subscribe('claim_rejected', (rejection) => {
|
|
292
300
|
if (!rejection.claimId) return;
|
|
293
|
-
|
|
301
|
+
const own = ownClaims.get(rejection.claimId);
|
|
302
|
+
if (own) {
|
|
294
303
|
const tgt = rejection.target
|
|
295
304
|
? claimLabel(rejection.target.entityType, rejection.target.entityId, rejection.target.field)
|
|
296
305
|
: rejection.claimId;
|
|
@@ -299,9 +308,17 @@ export function createClaimStream(
|
|
|
299
308
|
{ claimId: rejection.claimId, reason: rejection.reason },
|
|
300
309
|
);
|
|
301
310
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
own?.lifetime.end(
|
|
312
|
+
new AbloClaimedError(
|
|
313
|
+
rejection.message ?? `Claim ${rejection.claimId} was rejected.`,
|
|
314
|
+
{
|
|
315
|
+
code:
|
|
316
|
+
rejection.reason === 'conflict'
|
|
317
|
+
? 'claim_conflict'
|
|
318
|
+
: 'claim_lease_unavailable',
|
|
319
|
+
},
|
|
320
|
+
),
|
|
321
|
+
);
|
|
305
322
|
ownClaims.delete(rejection.claimId);
|
|
306
323
|
// A holder on another server may have claimed before this client joined
|
|
307
324
|
// the row group, so its one-shot presence frame was missed. A conflict
|
|
@@ -336,15 +353,20 @@ export function createClaimStream(
|
|
|
336
353
|
// The frame is validated by the transport's dispatcher against the same
|
|
337
354
|
// schema this parameter's type comes from, so it arrives proven.
|
|
338
355
|
t.subscribe('claim_lost', (lost) => {
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
const own = ownClaims.get(lost.claimId);
|
|
357
|
+
if (own) {
|
|
358
|
+
const c = own.claim;
|
|
341
359
|
logger.info(
|
|
342
|
-
`claim: lost ${
|
|
360
|
+
`claim: lost ${claimLabel(c.entityType, c.entityId, c.field)} (preempted or expired)`,
|
|
343
361
|
{ claimId: lost.claimId },
|
|
344
362
|
);
|
|
345
363
|
}
|
|
346
|
-
|
|
347
|
-
|
|
364
|
+
own?.lifetime.end(
|
|
365
|
+
new AbloClaimedError(
|
|
366
|
+
`Claim ${lost.claimId} was lost because it ${lost.reason === 'expired' ? 'expired' : 'was preempted'}. Re-acquire it before continuing.`,
|
|
367
|
+
{ code: 'claim_lost' },
|
|
368
|
+
),
|
|
369
|
+
);
|
|
348
370
|
ownClaims.delete(lost.claimId);
|
|
349
371
|
for (const l of lostListeners) {
|
|
350
372
|
try {
|
|
@@ -418,25 +440,35 @@ export function createClaimStream(
|
|
|
418
440
|
});
|
|
419
441
|
return;
|
|
420
442
|
}
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
),
|
|
443
|
+
const own = ownClaims.get(ack.claimId);
|
|
444
|
+
const c = own?.claim;
|
|
445
|
+
const error = new AbloClaimedError(
|
|
446
|
+
`The lease behind ${c ? claimLabel(c.entityType, c.entityId, c.field) : `claim ${ack.claimId}`} is no longer held — it expired or was granted onward while this participant was working. Re-acquire the claim and retry; a write attempted under the old lease is rejected by its \`readAt\` guard.`,
|
|
447
|
+
{ code: 'claim_lost' },
|
|
427
448
|
);
|
|
449
|
+
own?.lifetime.end(error);
|
|
450
|
+
ownClaims.delete(ack.claimId);
|
|
451
|
+
reject(error);
|
|
428
452
|
});
|
|
429
453
|
}),
|
|
430
454
|
);
|
|
431
455
|
|
|
432
|
-
// (3)
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
456
|
+
// (3) A granted handle belongs to this connection's exact grant. The server
|
|
457
|
+
// releases the participant's claims when its last socket leaves; trying
|
|
458
|
+
// to recreate them on `connected` silently swapped the lease beneath an
|
|
459
|
+
// old `data` / `readAt` / fenceToken tuple. End them instead. A caller
|
|
460
|
+
// reconnects, re-reads, and takes a fresh handle.
|
|
436
461
|
unsubs.push(
|
|
437
|
-
t.subscribe('
|
|
438
|
-
|
|
439
|
-
|
|
462
|
+
t.subscribe('disconnected', () => {
|
|
463
|
+
const error = new AbloConnectionError(
|
|
464
|
+
'The connection holding this claim ended. Reconnect and acquire a fresh claim before continuing.',
|
|
465
|
+
);
|
|
466
|
+
for (const { lifetime } of ownClaims.values()) {
|
|
467
|
+
lifetime.end(error);
|
|
468
|
+
}
|
|
469
|
+
ownClaims.clear();
|
|
470
|
+
for (const claimId of [...pendingHeartbeats.keys()]) {
|
|
471
|
+
settleHeartbeat(claimId, ({ reject }) => reject(error));
|
|
440
472
|
}
|
|
441
473
|
}),
|
|
442
474
|
);
|
|
@@ -480,8 +512,8 @@ export function createClaimStream(
|
|
|
480
512
|
|
|
481
513
|
/**
|
|
482
514
|
* Send one heartbeat and await its ack. Rejects with
|
|
483
|
-
* {@link AbloConnectionError}
|
|
484
|
-
*
|
|
515
|
+
* {@link AbloConnectionError} when the socket is down or the ack times out,
|
|
516
|
+
* and with
|
|
485
517
|
* {@link AbloClaimedError} (definitive) when the server answers that the
|
|
486
518
|
* lease is no longer ours.
|
|
487
519
|
*/
|
|
@@ -493,7 +525,7 @@ export function createClaimStream(
|
|
|
493
525
|
if (!attached?.isConnected()) {
|
|
494
526
|
return Promise.reject(
|
|
495
527
|
new AbloConnectionError(
|
|
496
|
-
`The heartbeat for ${claimLabel(claim.entityType, claim.entityId, claim.field)} was skipped because the connection is down.
|
|
528
|
+
`The heartbeat for ${claimLabel(claim.entityType, claim.entityId, claim.field)} was skipped because the connection is down. This connection's grant has ended; reconnect and acquire a fresh claim.`,
|
|
497
529
|
),
|
|
498
530
|
);
|
|
499
531
|
}
|
|
@@ -567,7 +599,8 @@ export function createClaimStream(
|
|
|
567
599
|
estimatedMs,
|
|
568
600
|
queue: args.queue,
|
|
569
601
|
};
|
|
570
|
-
|
|
602
|
+
const lifetime = createClaimLifetime();
|
|
603
|
+
ownClaims.set(claimId, { claim, lifetime });
|
|
571
604
|
sendBegin(claimId, claim);
|
|
572
605
|
// Coordination trace (info): the creator can see their human/agent claims.
|
|
573
606
|
logger.info(
|
|
@@ -580,6 +613,7 @@ export function createClaimStream(
|
|
|
580
613
|
const revoke = () => {
|
|
581
614
|
if (revoked) return;
|
|
582
615
|
revoked = true;
|
|
616
|
+
lifetime.end();
|
|
583
617
|
ownClaims.delete(claimId);
|
|
584
618
|
sendAbandon(claimId, claim);
|
|
585
619
|
logger.info(
|
|
@@ -588,7 +622,7 @@ export function createClaimStream(
|
|
|
588
622
|
);
|
|
589
623
|
};
|
|
590
624
|
|
|
591
|
-
return {
|
|
625
|
+
return bindClaimLifetime({
|
|
592
626
|
object: 'claim',
|
|
593
627
|
id: claimId,
|
|
594
628
|
status: 'active',
|
|
@@ -607,7 +641,7 @@ export function createClaimStream(
|
|
|
607
641
|
[Symbol.asyncDispose]: async () => {
|
|
608
642
|
revoke();
|
|
609
643
|
},
|
|
610
|
-
};
|
|
644
|
+
}, lifetime);
|
|
611
645
|
}
|
|
612
646
|
|
|
613
647
|
function resolveTarget(target: PresenceTarget): ClaimTarget {
|
|
@@ -686,6 +720,10 @@ export function createClaimStream(
|
|
|
686
720
|
);
|
|
687
721
|
});
|
|
688
722
|
}
|
|
723
|
+
const error = new AbloConnectionError(
|
|
724
|
+
'The claim stream was disposed. Acquire a fresh claim before continuing.',
|
|
725
|
+
);
|
|
726
|
+
for (const { lifetime } of ownClaims.values()) lifetime.end(error);
|
|
689
727
|
listeners.clear();
|
|
690
728
|
rejectionListeners.clear();
|
|
691
729
|
lostListeners.clear();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JoinOptions } from '@abloatai/transaction/resources/modelOperations';
|
|
1
|
+
import type { JoinOptions } from '@abloatai/transaction/client/resources/modelOperations';
|
|
2
2
|
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
3
3
|
import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
4
4
|
import { scopeKindOf, type ModelDef } from '@abloatai/transaction/schema/model';
|
|
@@ -10,7 +10,7 @@ import { z } from 'zod';
|
|
|
10
10
|
import { globalRuntime } from "../context.js";
|
|
11
11
|
import type { RuntimeContext } from "../RuntimeContext.js";
|
|
12
12
|
import { AbloValidationError } from "@abloatai/transaction/errors";
|
|
13
|
-
import { syncDeltaWireCoreSchema } from '@abloatai/transaction/
|
|
13
|
+
import { syncDeltaWireCoreSchema } from '@abloatai/transaction/observation';
|
|
14
14
|
|
|
15
15
|
// ─── Server Delta Schema ─────────────────────────────────────────────────────
|
|
16
16
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DurableWriteStore,
|
|
3
|
-
} from '@abloatai/transaction/
|
|
4
|
-
import type { PendingWrite } from '@abloatai/transaction/
|
|
3
|
+
} from '@abloatai/transaction/commit';
|
|
4
|
+
import type { PendingWrite } from '@abloatai/transaction/commit';
|
|
5
5
|
|
|
6
6
|
export interface CommitOutboxDatabase {
|
|
7
7
|
sealTransactionRecord(
|
|
@@ -27,11 +27,11 @@ import {
|
|
|
27
27
|
type LogPositionPort,
|
|
28
28
|
} from '../../logPosition.js';
|
|
29
29
|
import type { WriteOptions } from '../../interfaces/index.js';
|
|
30
|
-
import type {
|
|
30
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
|
|
31
31
|
import {
|
|
32
32
|
type CommitOperationResult,
|
|
33
33
|
type MutationCommitResult,
|
|
34
|
-
} from '@abloatai/transaction/
|
|
34
|
+
} from '@abloatai/transaction/commit';
|
|
35
35
|
import {
|
|
36
36
|
computePriorityScore,
|
|
37
37
|
normalizeModelKey,
|
|
@@ -74,9 +74,8 @@ import {
|
|
|
74
74
|
createCommitEnvelopeMember,
|
|
75
75
|
type DurableCommitEnvelope,
|
|
76
76
|
type CommitOutboxScope,
|
|
77
|
-
} from '@abloatai/transaction/
|
|
77
|
+
} from '@abloatai/transaction/commit';
|
|
78
78
|
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
79
|
-
import { stableStringify } from '@abloatai/transaction/utils/json';
|
|
80
79
|
import {
|
|
81
80
|
createLocalMutationPort,
|
|
82
81
|
type LocalMutationPort,
|
|
@@ -108,7 +107,7 @@ import {
|
|
|
108
107
|
import { enqueueTransaction, type QueueCoalescingContext } from './queueCoalescing.js';
|
|
109
108
|
import { processBatch, type BatchProcessingContext } from './batchProcessing.js';
|
|
110
109
|
import { handleFailure, type FailureHandlingContext } from './failureHandling.js';
|
|
111
|
-
import { handleConflict as resolveConflict, isPermanentError as classifyPermanentError, isDefinitiveRejection as classifyDefinitiveRejection, type
|
|
110
|
+
import { handleConflict as resolveConflict, isPermanentError as classifyPermanentError, isDefinitiveRejection as classifyDefinitiveRejection, type ConflictResolutionContext } from './failurePolicy.js';
|
|
112
111
|
import { takeNextExecutionBatch as selectExecutionBatch, takePendingDrainBatch as selectPendingDrainBatch } from './executionSelection.js';
|
|
113
112
|
import { scheduleProcessing as scheduleProcessingExternal, type ProcessingSchedulerContext } from './processingScheduler.js';
|
|
114
113
|
import {
|
|
@@ -314,7 +313,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
314
313
|
retryBackoff: this.config.retryBackoff,
|
|
315
314
|
},
|
|
316
315
|
commitLane: this.commitLane,
|
|
317
|
-
commitNotifications: this.commitNotifications,
|
|
318
316
|
commitMissingIds: this.commitMissingIds,
|
|
319
317
|
commitProcessing: this.commitProcessing,
|
|
320
318
|
setCommitProcessing: (value) => { this.commitProcessing = value; },
|
|
@@ -325,7 +323,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
325
323
|
await this.dispatchCommitBounded(envelope.operations, {
|
|
326
324
|
idempotencyKey: envelope.idempotencyKey,
|
|
327
325
|
...(envelope.commitOptions.reads ? { reads: envelope.commitOptions.reads } : {}),
|
|
328
|
-
...(envelope.commitOptions.track ? { track: envelope.commitOptions.track } : {}),
|
|
329
326
|
}),
|
|
330
327
|
),
|
|
331
328
|
persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
|
|
@@ -350,7 +347,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
350
347
|
private get commitReceiptContext(): CommitReceiptContext {
|
|
351
348
|
return {
|
|
352
349
|
commitStore: this.commitStore,
|
|
353
|
-
commitNotifications: this.commitNotifications,
|
|
354
350
|
commitMissingIds: this.commitMissingIds,
|
|
355
351
|
replicationLagErrors: this.replicationLagErrors,
|
|
356
352
|
on: (event, listener) => this.on(event, listener),
|
|
@@ -437,7 +433,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
437
433
|
store: this.store,
|
|
438
434
|
enqueue: (transaction) => { this.enqueue(transaction); },
|
|
439
435
|
optimisticUpdates: this.localMutationPort.updates,
|
|
440
|
-
commitNotifications: this.commitNotifications,
|
|
441
436
|
commitMissingIds: this.commitMissingIds,
|
|
442
437
|
sourceMutationIdsFor: (batch) => this.sourceMutationIdsFor(batch),
|
|
443
438
|
dispatchCommitBounded: (...args) => this.dispatchCommitBounded(...args),
|
|
@@ -447,8 +442,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
447
442
|
assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
|
|
448
443
|
sealDurableCommit: (input) => this.sealDurableCommit(input),
|
|
449
444
|
noteAck: (syncId) => { this.noteAck(syncId); },
|
|
450
|
-
classifyReceiptNotifications: (operations, notifications) => this.classifyReceiptNotifications(operations, notifications),
|
|
451
|
-
receiptTargetKey: (modelName, modelId) => this.receiptTargetKey(modelName, modelId),
|
|
452
445
|
scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
|
|
453
446
|
scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => { this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs); },
|
|
454
447
|
clearReplicationLagState: (transactionId) => { this.clearReplicationLagState(transactionId); },
|
|
@@ -479,7 +472,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
479
472
|
};
|
|
480
473
|
}
|
|
481
474
|
|
|
482
|
-
private get
|
|
475
|
+
private get conflictResolutionContext(): ConflictResolutionContext {
|
|
483
476
|
return {
|
|
484
477
|
config: this.config,
|
|
485
478
|
store: this.store,
|
|
@@ -623,80 +616,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
623
616
|
return entityKey(modelName, modelId);
|
|
624
617
|
}
|
|
625
618
|
|
|
626
|
-
|
|
627
|
-
private receiptTargetKey(modelName: string, modelId: string): string {
|
|
628
|
-
return stableStringify([normalizeModelKey(modelName), modelId]);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Relates stale notifications back to write targets without assuming the
|
|
633
|
-
* server's canonical model name uses the same spelling as the public schema
|
|
634
|
-
* key (`Item` versus `items`). Exact `(model,id)` wins; a globally unique id
|
|
635
|
-
* is the compatibility fallback. An ambiguous same-id cross-model mismatch
|
|
636
|
-
* is deliberately left unclassified, so it cannot falsely settle a queued
|
|
637
|
-
* write. A notification with no write-target id (or an explicit group) is a
|
|
638
|
-
* declared-read conflict and holds the whole batch.
|
|
639
|
-
*/
|
|
640
|
-
private classifyReceiptNotifications(
|
|
641
|
-
operations: readonly { model: string; id: string }[],
|
|
642
|
-
notifications: readonly StaleNotification[],
|
|
643
|
-
): {
|
|
644
|
-
holdsEntireBatch: boolean;
|
|
645
|
-
heldTargets: Set<string>;
|
|
646
|
-
notificationsByTarget: Map<string, StaleNotification[]>;
|
|
647
|
-
} {
|
|
648
|
-
const targets = operations.map((operation) => ({
|
|
649
|
-
id: operation.id,
|
|
650
|
-
key: this.receiptTargetKey(operation.model, operation.id),
|
|
651
|
-
}));
|
|
652
|
-
const heldTargets = new Set<string>();
|
|
653
|
-
const notificationsByTarget = new Map<string, StaleNotification[]>();
|
|
654
|
-
let holdsEntireBatch = false;
|
|
655
|
-
|
|
656
|
-
for (const notification of notifications) {
|
|
657
|
-
// Scope is decided before any target matching. A group premise fires over
|
|
658
|
-
// the WHOLE batch by convention, and its `target` now names the row that
|
|
659
|
-
// actually moved — which may well be a row this batch is writing, so
|
|
660
|
-
// matching first would misread a batch-wide hold as a per-row one.
|
|
661
|
-
if (notification.scope === 'group') {
|
|
662
|
-
holdsEntireBatch = true;
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
const candidates = targets.filter(
|
|
666
|
-
(target) => target.id === notification.target.id,
|
|
667
|
-
);
|
|
668
|
-
const notificationKey = this.receiptTargetKey(
|
|
669
|
-
notification.target.model,
|
|
670
|
-
notification.target.id,
|
|
671
|
-
);
|
|
672
|
-
const exactTargets = candidates.filter(
|
|
673
|
-
(target) => target.key === notificationKey,
|
|
674
|
-
);
|
|
675
|
-
const candidateKeys = new Set(
|
|
676
|
-
(exactTargets.length > 0 ? exactTargets : candidates).map(
|
|
677
|
-
(target) => target.key,
|
|
678
|
-
),
|
|
679
|
-
);
|
|
680
|
-
|
|
681
|
-
if (candidates.length === 0) {
|
|
682
|
-
holdsEntireBatch = true;
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
if (candidateKeys.size !== 1) {
|
|
686
|
-
// Same id across multiple differently named models with no exact match:
|
|
687
|
-
// the id-only compatibility fallback is ambiguous. Await the echo.
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
const [targetKey] = candidateKeys;
|
|
691
|
-
if (!targetKey) continue;
|
|
692
|
-
heldTargets.add(targetKey);
|
|
693
|
-
const targetNotifications = notificationsByTarget.get(targetKey) ?? [];
|
|
694
|
-
targetNotifications.push(notification);
|
|
695
|
-
notificationsByTarget.set(targetKey, targetNotifications);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return { holdsEntireBatch, heldTargets, notificationsByTarget };
|
|
699
|
-
}
|
|
619
|
+
/* Stale receipt classification was removed; stale premises now reject. */
|
|
700
620
|
|
|
701
621
|
private resolveConfirmation(transaction: QueuedMutation): void {
|
|
702
622
|
const resolver = this.confirmationResolvers.get(transaction.id);
|
|
@@ -807,11 +727,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
807
727
|
|
|
808
728
|
private readonly localMutationPort: LocalMutationPort;
|
|
809
729
|
|
|
810
|
-
// Stale-context notifications, keyed by transaction id. When the server
|
|
811
|
-
// accepts a commit but reports that an operation's premise had moved,
|
|
812
|
-
// the notification lands here from the commit acknowledgement and is drained
|
|
813
|
-
// by `waitForCommitReceipt`, so the receipt can carry it back to the caller.
|
|
814
|
-
private commitNotifications = new Map<string, StaleNotification[]>();
|
|
815
730
|
/** Zero-row targets returned on a successful atomic commit receipt. */
|
|
816
731
|
private commitMissingIds = new Map<string, string[]>();
|
|
817
732
|
|
|
@@ -1674,7 +1589,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1674
1589
|
async enqueueCommit(
|
|
1675
1590
|
clientTxId: string,
|
|
1676
1591
|
operations: CommitTransaction['operations'],
|
|
1677
|
-
options: { reads?: ReadDependency[] | null
|
|
1592
|
+
options: { reads?: ReadDependency[] | null } = {},
|
|
1678
1593
|
): Promise<void> {
|
|
1679
1594
|
return enqueueCommit(this.commitApiContext, clientTxId, operations, options);
|
|
1680
1595
|
}
|
|
@@ -1687,7 +1602,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
1687
1602
|
clientTxId: string,
|
|
1688
1603
|
): Promise<{
|
|
1689
1604
|
lastSyncId: number;
|
|
1690
|
-
notifications?: StaleNotification[];
|
|
1691
1605
|
missingIds?: string[];
|
|
1692
1606
|
operationResults?: CommitOperationResult[];
|
|
1693
1607
|
}> {
|
|
@@ -1712,7 +1626,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1712
1626
|
}
|
|
1713
1627
|
|
|
1714
1628
|
async handleConflict(transaction: QueuedMutation, serverData: MutationInput): Promise<void> {
|
|
1715
|
-
await resolveConflict(this.
|
|
1629
|
+
await resolveConflict(this.conflictResolutionContext, transaction, serverData);
|
|
1716
1630
|
}
|
|
1717
1631
|
|
|
1718
1632
|
private applyOptimisticCreate(model: LocalModel, transaction: QueuedMutation): void {
|
|
@@ -2013,7 +1927,6 @@ export class MutationQueue extends EventEmitter {
|
|
|
2013
1927
|
this.recentDeltaCorrelations.clear();
|
|
2014
1928
|
this.commitLane = [];
|
|
2015
1929
|
this.commitStore.clear();
|
|
2016
|
-
this.commitNotifications.clear();
|
|
2017
1930
|
this.commitMissingIds.clear();
|
|
2018
1931
|
|
|
2019
1932
|
// Clear event listeners
|
|
@@ -3,10 +3,9 @@ import type { MutationQueueConfig } from './MutationQueue.js';
|
|
|
3
3
|
import type { MutationInput, QueuedMutation } from './commitPayload.js';
|
|
4
4
|
import type { MutationStore } from './MutationStore.js';
|
|
5
5
|
import type { OptimisticUpdateEntry } from './localMutation.js';
|
|
6
|
-
import type { StaleNotification } from '@abloatai/transaction/coordination/schema';
|
|
7
6
|
import type { DeltaConfirmationTracker } from './deltaConfirmation.js';
|
|
8
|
-
import type { MutationCommitResult } from '@abloatai/transaction/
|
|
9
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/
|
|
7
|
+
import type { MutationCommitResult } from '@abloatai/transaction/commit';
|
|
8
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
|
|
10
9
|
import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
|
|
11
10
|
import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP, type WriteOperationFields } from './commitPayload.js';
|
|
12
11
|
import type { MutationOperationType } from '@abloatai/transaction/types';
|
|
@@ -30,7 +29,6 @@ export interface BatchProcessingContext {
|
|
|
30
29
|
readonly store: MutationStore;
|
|
31
30
|
readonly enqueue: (transaction: QueuedMutation) => void;
|
|
32
31
|
readonly optimisticUpdates: Map<string, OptimisticUpdateEntry>;
|
|
33
|
-
readonly commitNotifications: Map<string, StaleNotification[]>;
|
|
34
32
|
readonly commitMissingIds: Map<string, string[]>;
|
|
35
33
|
readonly sourceMutationIdsFor: (batch: readonly QueuedMutation[]) => string[];
|
|
36
34
|
readonly dispatchCommitBounded: (...args: Parameters<import('../../interfaces/index.js').MutationExecutor['commit']>) => ReturnType<import('../../interfaces/index.js').MutationExecutor['commit']>;
|
|
@@ -40,11 +38,6 @@ export interface BatchProcessingContext {
|
|
|
40
38
|
readonly assertEnvelopeInsideReplayWindow: (envelope: DurableCommitEnvelope) => void;
|
|
41
39
|
readonly sealDurableCommit: (input: Parameters<typeof import('./commitTransport.js').sealDurableCommit>[1]) => Promise<DurableCommitEnvelope>;
|
|
42
40
|
readonly noteAck: (syncId: number | undefined) => void;
|
|
43
|
-
readonly classifyReceiptNotifications: (
|
|
44
|
-
operations: readonly { model: string; id: string }[],
|
|
45
|
-
notifications: readonly StaleNotification[],
|
|
46
|
-
) => { holdsEntireBatch: boolean; heldTargets: Set<string>; notificationsByTarget: Map<string, StaleNotification[]> };
|
|
47
|
-
readonly receiptTargetKey: (modelName: string, modelId: string) => string;
|
|
48
41
|
readonly scheduleReplicationLagTimeout: (transactionId: string, clientTxId?: string, correlationId?: string) => void;
|
|
49
42
|
readonly scheduleDeltaConfirmationTimeout: (transaction: QueuedMutation, timeoutMs: number) => void;
|
|
50
43
|
readonly clearReplicationLagState: (transactionId: string) => void;
|
|
@@ -170,24 +163,6 @@ export async function processBatch(ctx: BatchProcessingContext): Promise<void> {
|
|
|
170
163
|
);
|
|
171
164
|
const lastSyncId = result.lastSyncId;
|
|
172
165
|
|
|
173
|
-
const notifications = result.notifications;
|
|
174
|
-
const {
|
|
175
|
-
holdsEntireBatch,
|
|
176
|
-
heldTargets,
|
|
177
|
-
notificationsByTarget,
|
|
178
|
-
} = ctx.classifyReceiptNotifications(
|
|
179
|
-
batchOps.map(({ op }) => ({ model: op.model, id: op.id })),
|
|
180
|
-
notifications ?? [],
|
|
181
|
-
);
|
|
182
|
-
for (const { tx } of batchOps) {
|
|
183
|
-
const txTarget = ctx.receiptTargetKey(tx.modelKey, tx.modelId);
|
|
184
|
-
const txNotifs = holdsEntireBatch
|
|
185
|
-
? notifications
|
|
186
|
-
: notificationsByTarget.get(txTarget);
|
|
187
|
-
if (txNotifs && txNotifs.length > 0) {
|
|
188
|
-
ctx.commitNotifications.set(tx.id, txNotifs);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
166
|
const missingIds = new Set(result.missingIds ?? []);
|
|
192
167
|
const settlingBatchOps: typeof batchOps = [];
|
|
193
168
|
for (const entry of batchOps) {
|
|
@@ -202,17 +177,6 @@ export async function processBatch(ctx: BatchProcessingContext): Promise<void> {
|
|
|
202
177
|
);
|
|
203
178
|
continue;
|
|
204
179
|
}
|
|
205
|
-
if (
|
|
206
|
-
holdsEntireBatch ||
|
|
207
|
-
heldTargets.has(ctx.receiptTargetKey(tx.modelKey, tx.modelId))
|
|
208
|
-
) {
|
|
209
|
-
await ctx.rollbackOptimistic(tx, 'conflict_server_wins');
|
|
210
|
-
ctx.store.updateStatus(tx.id, 'completed');
|
|
211
|
-
ctx.emit('transaction:completed', tx);
|
|
212
|
-
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
213
|
-
ctx.optimisticUpdates.delete(tx.id);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
180
|
settlingBatchOps.push(entry);
|
|
217
181
|
}
|
|
218
182
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AbloIdempotencyError } from '@abloatai/transaction/errors';
|
|
2
|
-
import type { ReadDependency
|
|
2
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
|
|
3
3
|
import { stableStringify } from '@abloatai/transaction/utils/json';
|
|
4
|
-
import type { DurableCommitEnvelope } from '@abloatai/transaction/
|
|
4
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
|
|
5
5
|
import type { CommitTransaction } from './commitLane.js';
|
|
6
6
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
7
7
|
|
|
@@ -21,7 +21,7 @@ export async function enqueueCommit(
|
|
|
21
21
|
ctx: CommitApiContext,
|
|
22
22
|
clientTxId: string,
|
|
23
23
|
operations: CommitTransaction['operations'],
|
|
24
|
-
options: { reads?: ReadDependency[] | null
|
|
24
|
+
options: { reads?: ReadDependency[] | null } = {},
|
|
25
25
|
): Promise<void> {
|
|
26
26
|
ctx.assertDurableReplayOpen();
|
|
27
27
|
const existing = ctx.commitStore.get(clientTxId);
|
|
@@ -30,12 +30,10 @@ export async function enqueueCommit(
|
|
|
30
30
|
const existingIntent = stableStringify({
|
|
31
31
|
operations: existing.operations,
|
|
32
32
|
reads: existing.reads ?? null,
|
|
33
|
-
track: existing.track ?? null,
|
|
34
33
|
});
|
|
35
34
|
const incomingIntent = stableStringify({
|
|
36
35
|
operations,
|
|
37
36
|
reads: options.reads ?? null,
|
|
38
|
-
track: options.track ?? null,
|
|
39
37
|
});
|
|
40
38
|
if (existingIntent !== incomingIntent) {
|
|
41
39
|
throw new AbloIdempotencyError(
|
|
@@ -59,7 +57,6 @@ export async function enqueueCommit(
|
|
|
59
57
|
kind: 'commit',
|
|
60
58
|
operations: [...operations],
|
|
61
59
|
...(options.reads ? { reads: options.reads } : {}),
|
|
62
|
-
...(options.track ? { track: options.track } : {}),
|
|
63
60
|
status: 'pending',
|
|
64
61
|
createdAt: now,
|
|
65
62
|
attempts: 0,
|
|
@@ -73,7 +70,6 @@ export async function enqueueCommit(
|
|
|
73
70
|
operations: tx.operations,
|
|
74
71
|
commitOptions: {
|
|
75
72
|
...(tx.reads ? { reads: tx.reads } : {}),
|
|
76
|
-
...(tx.track ? { track: tx.track } : {}),
|
|
77
73
|
},
|
|
78
74
|
createdAt: tx.createdAt,
|
|
79
75
|
sealedAt: tx.sealedAt,
|