@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.
Files changed (34) hide show
  1. package/dist/local/BaseSyncedStore.d.ts +3 -0
  2. package/dist/local/BaseSyncedStore.js +1 -0
  3. package/dist/local/client/clientPrelude.js +5 -1
  4. package/dist/local/client/createModelProxy.d.ts +1 -3
  5. package/dist/local/client/createModelProxy.js +28 -23
  6. package/dist/local/client/options.d.ts +17 -32
  7. package/dist/local/client/reactiveEngine.js +1 -3
  8. package/dist/local/client/resourceTypes.d.ts +1 -1
  9. package/dist/local/client/storeLifecycle.d.ts +4 -0
  10. package/dist/local/client/storeLifecycle.js +30 -1
  11. package/dist/local/sync/createClaimStream.js +33 -24
  12. package/dist/local/transactions/mutations/MutationQueue.d.ts +20 -1
  13. package/dist/local/transactions/mutations/MutationQueue.js +20 -2
  14. package/dist/local/transactions/mutations/commitLane.d.ts +7 -0
  15. package/dist/local/transactions/mutations/commitLane.js +8 -5
  16. package/dist/local/transactions/mutations/commitPayload.d.ts +2 -0
  17. package/dist/local/transactions/mutations/failureHandling.d.ts +2 -1
  18. package/dist/local/transactions/mutations/failureHandling.js +18 -13
  19. package/dist/surface.d.ts +1 -1
  20. package/dist/surface.js +2 -1
  21. package/package.json +3 -2
  22. package/src/local/BaseSyncedStore.ts +4 -0
  23. package/src/local/client/clientPrelude.ts +5 -1
  24. package/src/local/client/createModelProxy.ts +30 -24
  25. package/src/local/client/options.ts +20 -34
  26. package/src/local/client/reactiveEngine.ts +0 -2
  27. package/src/local/client/resourceTypes.ts +1 -0
  28. package/src/local/client/storeLifecycle.ts +43 -0
  29. package/src/local/sync/createClaimStream.ts +55 -31
  30. package/src/local/transactions/mutations/MutationQueue.ts +30 -4
  31. package/src/local/transactions/mutations/commitLane.ts +21 -5
  32. package/src/local/transactions/mutations/commitPayload.ts +2 -0
  33. package/src/local/transactions/mutations/failureHandling.ts +27 -12
  34. 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 public `wait: 'confirmed'` deadline: expiry rejects
170
- // the waiter with `replication_lag_timeout` while the accepted write remains
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: { maxRetries: this.config.maxRetries },
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 `wait: 'confirmed'` promise without changing the
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: { maxRetries: number };
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
- if (!(error instanceof AbloConnectionError)) tx.transientAttempts = (tx.transientAttempts ?? 0) + 1;
169
- const exhausted = (tx.transientAttempts ?? 0) > ctx.config.maxRetries;
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 ?? 0, message: error.message,
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<MutationQueueConfig, 'enableOptimistic' | 'maxRetries' | 'retryBackoff'>;
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 `waitForConfirmation` (the
117
- // `wait:'confirmed'` path) listens on — without it a permanently
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
- if (transaction.attempts < ctx.config.maxRetries) {
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 { baseMs, capMs } = ctx.config.retryBackoff;
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<