@abloatai/humans 0.45.0 → 0.47.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/local/BaseSyncedStore.d.ts +3 -0
- package/dist/local/BaseSyncedStore.js +1 -0
- package/dist/local/client/clientPrelude.js +5 -1
- package/dist/local/client/createModelProxy.d.ts +1 -3
- package/dist/local/client/createModelProxy.js +28 -23
- package/dist/local/client/options.d.ts +17 -32
- package/dist/local/client/reactiveEngine.js +1 -3
- package/dist/local/client/resourceTypes.d.ts +1 -1
- package/dist/local/client/storeLifecycle.d.ts +4 -0
- package/dist/local/client/storeLifecycle.js +30 -1
- package/dist/local/sync/createClaimStream.js +33 -24
- package/dist/local/transactions/mutations/MutationQueue.d.ts +20 -1
- package/dist/local/transactions/mutations/MutationQueue.js +20 -2
- package/dist/local/transactions/mutations/commitLane.d.ts +7 -0
- package/dist/local/transactions/mutations/commitLane.js +8 -5
- package/dist/local/transactions/mutations/commitPayload.d.ts +2 -0
- package/dist/local/transactions/mutations/failureHandling.d.ts +2 -1
- package/dist/local/transactions/mutations/failureHandling.js +18 -13
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -1
- package/package.json +3 -2
- package/src/local/BaseSyncedStore.ts +4 -0
- package/src/local/client/clientPrelude.ts +5 -1
- package/src/local/client/createModelProxy.ts +30 -24
- package/src/local/client/options.ts +20 -34
- package/src/local/client/reactiveEngine.ts +0 -2
- package/src/local/client/resourceTypes.ts +1 -0
- package/src/local/client/storeLifecycle.ts +43 -0
- package/src/local/sync/createClaimStream.ts +55 -31
- package/src/local/transactions/mutations/MutationQueue.ts +30 -4
- package/src/local/transactions/mutations/commitLane.ts +21 -5
- package/src/local/transactions/mutations/commitPayload.ts +2 -0
- package/src/local/transactions/mutations/failureHandling.ts +27 -12
- package/src/surface.ts +2 -1
|
@@ -158,6 +158,15 @@ export interface MutationQueueConfig {
|
|
|
158
158
|
maxBatchSize: number;
|
|
159
159
|
batchDelay: number;
|
|
160
160
|
maxRetries: number;
|
|
161
|
+
/**
|
|
162
|
+
* Minimum wall-clock window for retrying transient write failures with the
|
|
163
|
+
* same durable envelope and idempotency key. This absorbs managed-database
|
|
164
|
+
* promotion and brief regional network incidents without double-applying a
|
|
165
|
+
* write. Defaults to 120 seconds: the Aurora promotion drill recovered
|
|
166
|
+
* writes just beyond 60 seconds, so a one-minute boundary discarded exact
|
|
167
|
+
* envelopes at the instant the new writer became usable.
|
|
168
|
+
*/
|
|
169
|
+
availabilityRetryWindowMs: number;
|
|
161
170
|
conflictResolution: ConflictResolution;
|
|
162
171
|
enablePersistence: boolean;
|
|
163
172
|
enableOptimistic: boolean;
|
|
@@ -166,8 +175,8 @@ export interface MutationQueueConfig {
|
|
|
166
175
|
maxExecutingTransactions: number;
|
|
167
176
|
// How long to wait, in milliseconds, for a change's confirming sync delta
|
|
168
177
|
// before the retry-and-reconciliation cycle begins. For a source-forwarded
|
|
169
|
-
// write this is also the
|
|
170
|
-
//
|
|
178
|
+
// write this is also the awaited model-write deadline: expiry rejects the
|
|
179
|
+
// waiter with `replication_lag_timeout` while the accepted write remains
|
|
171
180
|
// pending. Defaults to 30000 (30 seconds); raise it for slow networks.
|
|
172
181
|
deltaConfirmationTimeout: number;
|
|
173
182
|
/**
|
|
@@ -289,6 +298,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
289
298
|
private replicationLagTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
290
299
|
private replicationLagErrors = new Map<string, AbloConnectionError>();
|
|
291
300
|
private commitProcessing = false;
|
|
301
|
+
private commitRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
292
302
|
private lastCommitSequence = 0;
|
|
293
303
|
private durableReplayBlock: AbloIdempotencyError | null = null;
|
|
294
304
|
/** Browser-backed strict outbox; absent for standalone/in-memory consumers. */
|
|
@@ -312,7 +322,11 @@ export class MutationQueue extends EventEmitter {
|
|
|
312
322
|
private get commitLaneContext(): CommitLaneContext {
|
|
313
323
|
return {
|
|
314
324
|
runtime: this.runtime,
|
|
315
|
-
config: {
|
|
325
|
+
config: {
|
|
326
|
+
maxRetries: this.config.maxRetries,
|
|
327
|
+
availabilityRetryWindowMs: this.config.availabilityRetryWindowMs,
|
|
328
|
+
retryBackoff: this.config.retryBackoff,
|
|
329
|
+
},
|
|
316
330
|
commitLane: this.commitLane,
|
|
317
331
|
commitNotifications: this.commitNotifications,
|
|
318
332
|
commitMissingIds: this.commitMissingIds,
|
|
@@ -336,6 +350,13 @@ export class MutationQueue extends EventEmitter {
|
|
|
336
350
|
noteAck: (syncId) => this.noteAck(syncId),
|
|
337
351
|
isDefinitiveRejection: (error) => this.isDefinitiveRejection(error),
|
|
338
352
|
isPermanentError: (error) => this.isPermanentError(error),
|
|
353
|
+
scheduleRetry: (delayMs) => {
|
|
354
|
+
if (this.commitRetryTimer !== null) clearTimeout(this.commitRetryTimer);
|
|
355
|
+
this.commitRetryTimer = setTimeout(() => {
|
|
356
|
+
this.commitRetryTimer = null;
|
|
357
|
+
void this.processCommitLane();
|
|
358
|
+
}, delayMs);
|
|
359
|
+
},
|
|
339
360
|
emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
|
|
340
361
|
};
|
|
341
362
|
}
|
|
@@ -782,6 +803,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
782
803
|
maxBatchSize: 50, // send up to this many operations per commit
|
|
783
804
|
batchDelay: 150, // milliseconds to wait for more operations before sending
|
|
784
805
|
maxRetries: 3,
|
|
806
|
+
availabilityRetryWindowMs: 120_000,
|
|
785
807
|
conflictResolution: {
|
|
786
808
|
strategy: 'last-write-wins',
|
|
787
809
|
},
|
|
@@ -1065,7 +1087,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1065
1087
|
}
|
|
1066
1088
|
|
|
1067
1089
|
/**
|
|
1068
|
-
* Bounds the public
|
|
1090
|
+
* Bounds the public model-write confirmation promise without changing the
|
|
1069
1091
|
* accepted write's lifecycle. A lag timeout is not a rejection from the
|
|
1070
1092
|
* source database, so it must never emit `transaction:failed`, roll back
|
|
1071
1093
|
* optimistic state, or remove the durable replay envelope.
|
|
@@ -2004,6 +2026,10 @@ export class MutationQueue extends EventEmitter {
|
|
|
2004
2026
|
clearTimeout(this.commitOfflineGraceTimer);
|
|
2005
2027
|
this.commitOfflineGraceTimer = null;
|
|
2006
2028
|
}
|
|
2029
|
+
if (this.commitRetryTimer !== null) {
|
|
2030
|
+
clearTimeout(this.commitRetryTimer);
|
|
2031
|
+
this.commitRetryTimer = null;
|
|
2032
|
+
}
|
|
2007
2033
|
|
|
2008
2034
|
// Clear store
|
|
2009
2035
|
this.store.clear();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
2
|
import type { ReadDependency, TrackDependency, OnStaleMode, StaleNotification } from '@abloatai/transaction/coordination/schema';
|
|
3
|
-
import { AbloConnectionError } from '@abloatai/transaction/errors';
|
|
4
3
|
import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
5
4
|
import type {
|
|
6
5
|
DurableCommitEnvelope,
|
|
7
6
|
DurableCommitOperation,
|
|
8
7
|
} from '@abloatai/transaction/transactions/settlement/commitEnvelope';
|
|
9
8
|
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
9
|
+
import { transientRetryDelayMs } from './failureHandling.js';
|
|
10
10
|
|
|
11
11
|
export interface CommitTransaction {
|
|
12
12
|
id: string;
|
|
@@ -26,6 +26,7 @@ export interface CommitTransaction {
|
|
|
26
26
|
createdAt: number;
|
|
27
27
|
attempts: number;
|
|
28
28
|
transientAttempts?: number;
|
|
29
|
+
firstTransientFailureAt?: number;
|
|
29
30
|
lastSyncId?: number;
|
|
30
31
|
correlationId?: string;
|
|
31
32
|
error?: Error;
|
|
@@ -38,7 +39,11 @@ export interface CommitTransaction {
|
|
|
38
39
|
|
|
39
40
|
export interface CommitLaneContext {
|
|
40
41
|
readonly runtime: RuntimeContext;
|
|
41
|
-
readonly config: {
|
|
42
|
+
readonly config: {
|
|
43
|
+
maxRetries: number;
|
|
44
|
+
availabilityRetryWindowMs: number;
|
|
45
|
+
retryBackoff: { baseMs: number; capMs: number };
|
|
46
|
+
};
|
|
42
47
|
readonly commitLane: CommitTransaction[];
|
|
43
48
|
readonly commitNotifications: Map<string, StaleNotification[]>;
|
|
44
49
|
readonly commitMissingIds: Map<string, string[]>;
|
|
@@ -56,6 +61,7 @@ export interface CommitLaneContext {
|
|
|
56
61
|
readonly noteAck: (syncId: number | undefined) => void;
|
|
57
62
|
readonly isDefinitiveRejection: (error: Error) => boolean;
|
|
58
63
|
readonly isPermanentError: (error: Error) => boolean;
|
|
64
|
+
readonly scheduleRetry: (delayMs: number) => void;
|
|
59
65
|
readonly emitCommitLifecycle: (event: string, payload: object) => void;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -165,14 +171,24 @@ export async function processCommitLane(ctx: CommitLaneContext): Promise<void> {
|
|
|
165
171
|
} catch (cause) {
|
|
166
172
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
167
173
|
if (dispatchStarted && ctx.isDefinitiveRejection(error)) await ctx.removeDurableCommit(tx.id);
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
tx.transientAttempts = (tx.transientAttempts ?? 0) + 1;
|
|
175
|
+
tx.firstTransientFailureAt ??= Date.now();
|
|
176
|
+
const outsideAvailabilityWindow =
|
|
177
|
+
Date.now() - tx.firstTransientFailureAt >= ctx.config.availabilityRetryWindowMs;
|
|
178
|
+
const exhausted =
|
|
179
|
+
tx.transientAttempts > ctx.config.maxRetries && outsideAvailabilityWindow;
|
|
170
180
|
if (!ctx.isPermanentError(error) && !exhausted) {
|
|
171
181
|
tx.status = 'pending';
|
|
182
|
+
const delayMs = transientRetryDelayMs(
|
|
183
|
+
error,
|
|
184
|
+
tx.transientAttempts,
|
|
185
|
+
ctx.config.retryBackoff,
|
|
186
|
+
);
|
|
172
187
|
ctx.runtime.logger.debug('[MutationQueue] commit lane transient', {
|
|
173
188
|
txId: tx.id.slice(0, 12), attempts: tx.attempts,
|
|
174
|
-
transientAttempts: tx.transientAttempts
|
|
189
|
+
transientAttempts: tx.transientAttempts, delayMs, message: error.message,
|
|
175
190
|
});
|
|
191
|
+
ctx.scheduleRetry(delayMs);
|
|
176
192
|
break;
|
|
177
193
|
}
|
|
178
194
|
tx.status = 'failed';
|
|
@@ -108,6 +108,8 @@ export interface QueuedMutation {
|
|
|
108
108
|
status: 'pending' | 'executing' | 'awaiting_delta' | 'completed' | 'failed' | 'rolled_back';
|
|
109
109
|
createdAt: number;
|
|
110
110
|
attempts: number;
|
|
111
|
+
/** First transient dispatch failure in the current availability incident. */
|
|
112
|
+
firstTransientFailureAt?: number;
|
|
111
113
|
priority: 'normal' | 'high';
|
|
112
114
|
priorityScore: number; // foreign-key-aware priority, derived, used for sorting
|
|
113
115
|
writeOptions?: WriteOptions;
|
|
@@ -7,7 +7,10 @@ import { extractStatusCode } from './commitPayload.js';
|
|
|
7
7
|
|
|
8
8
|
export interface FailureHandlingContext {
|
|
9
9
|
readonly runtime: RuntimeContext;
|
|
10
|
-
readonly config: Pick<
|
|
10
|
+
readonly config: Pick<
|
|
11
|
+
MutationQueueConfig,
|
|
12
|
+
'enableOptimistic' | 'maxRetries' | 'retryBackoff' | 'availabilityRetryWindowMs'
|
|
13
|
+
>;
|
|
11
14
|
readonly store: MutationStore;
|
|
12
15
|
readonly isPermanentError: (error: Error) => boolean;
|
|
13
16
|
readonly rollbackOptimistic: (transaction: QueuedMutation, reason: string, error?: Error) => Promise<void>;
|
|
@@ -17,6 +20,21 @@ export interface FailureHandlingContext {
|
|
|
17
20
|
readonly emit: (event: string, payload: object) => boolean;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
export function transientRetryDelayMs(
|
|
24
|
+
error: Error,
|
|
25
|
+
attempt: number,
|
|
26
|
+
retryBackoff: MutationQueueConfig['retryBackoff'],
|
|
27
|
+
): number {
|
|
28
|
+
const { baseMs, capMs } = retryBackoff;
|
|
29
|
+
let base = baseMs;
|
|
30
|
+
try {
|
|
31
|
+
const status = extractStatusCode(error);
|
|
32
|
+
if (status === 429 || status === 503) base = Math.max(baseMs, 1_000);
|
|
33
|
+
} catch {}
|
|
34
|
+
const ceiling = Math.min(capMs, base * Math.pow(2, Math.max(0, attempt - 1)));
|
|
35
|
+
return Math.floor(Math.random() * ceiling);
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export async function handleFailure(ctx: FailureHandlingContext, transaction: QueuedMutation, error: Error): Promise<void> {
|
|
21
39
|
transaction.attempts++;
|
|
22
40
|
|
|
@@ -113,27 +131,24 @@ export async function handleFailure(ctx: FailureHandlingContext, transaction: Qu
|
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
ctx.emit('transaction:failed', { transaction, error, permanent: true });
|
|
116
|
-
// The id-suffixed event is what
|
|
117
|
-
// `
|
|
134
|
+
// The id-suffixed event is what the awaited model-write promise listens
|
|
135
|
+
// on through `waitForConfirmation` — without it a permanently
|
|
118
136
|
// rejected write left the caller's promise hanging forever.
|
|
119
137
|
ctx.emit(`transaction:failed:${transaction.id}`, { error });
|
|
120
138
|
return;
|
|
121
139
|
}
|
|
122
140
|
|
|
123
|
-
|
|
141
|
+
transaction.firstTransientFailureAt ??= Date.now();
|
|
142
|
+
const insideAvailabilityWindow =
|
|
143
|
+
Date.now() - transaction.firstTransientFailureAt < ctx.config.availabilityRetryWindowMs;
|
|
144
|
+
|
|
145
|
+
if (transaction.attempts < ctx.config.maxRetries || insideAvailabilityWindow) {
|
|
124
146
|
// Exponential backoff with full jitter on every transient retry:
|
|
125
147
|
// `sleep = random(0, min(cap, base * 2^attempt))`. Throttling responses
|
|
126
148
|
// (429/503) use a longer base than other transient errors. The re-enqueue
|
|
127
149
|
// is scheduled rather than awaited, so one backing-off transaction cannot
|
|
128
150
|
// stall unrelated commits.
|
|
129
|
-
const
|
|
130
|
-
let base = baseMs;
|
|
131
|
-
try {
|
|
132
|
-
const status = extractStatusCode(error);
|
|
133
|
-
if (status === 429 || status === 503) base = Math.max(baseMs, 1_000);
|
|
134
|
-
} catch {}
|
|
135
|
-
const ceiling = Math.min(capMs, base * Math.pow(2, transaction.attempts - 1));
|
|
136
|
-
const delay = Math.floor(Math.random() * ceiling);
|
|
151
|
+
const delay = transientRetryDelayMs(error, transaction.attempts, ctx.config.retryBackoff);
|
|
137
152
|
|
|
138
153
|
ctx.store.updateStatus(transaction.id, 'pending');
|
|
139
154
|
setTimeout(() => {
|
package/src/surface.ts
CHANGED
|
@@ -76,6 +76,8 @@ type _ListOptionKeysExact = Expect<
|
|
|
76
76
|
export const PUBLIC_ABLO_OPTION_KEYS = [
|
|
77
77
|
'schema',
|
|
78
78
|
'apiKey',
|
|
79
|
+
'projectId',
|
|
80
|
+
'branchId',
|
|
79
81
|
'authEndpoint',
|
|
80
82
|
'authTimeoutMs',
|
|
81
83
|
'allowCrossOriginAuthEndpoint',
|
|
@@ -94,7 +96,6 @@ export const PUBLIC_ABLO_OPTION_KEYS = [
|
|
|
94
96
|
'dangerouslyAllowBrowser',
|
|
95
97
|
'collaborationEvents',
|
|
96
98
|
'plugins',
|
|
97
|
-
'wait',
|
|
98
99
|
] as const;
|
|
99
100
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
100
101
|
type _AbloOptionKeysExact = Expect<
|