@abloatai/humans 0.56.0 → 0.57.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.
@@ -936,7 +936,7 @@ export class Model {
936
936
  throw new AbloValidationError('Model identifier (__typename, __class, or modelName) not found in data', { code: 'model_identifier_missing' });
937
937
  }
938
938
  // Try to get model class by identifier
939
- let ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
939
+ const ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
940
940
  if (!ModelClass) {
941
941
  throw new AbloValidationError(`Model class not found for: ${modelIdentifier}`, { code: 'model_class_not_registered' });
942
942
  }
@@ -17,41 +17,11 @@ import { type CommitLatencySample } from './transactions/mutations/commitLatency
17
17
  import { type UnconfirmedWritesMetrics } from './transactions/mutations/UnconfirmedWrites.js';
18
18
  import type { DurableWriteStore } from './transactions/mutations/durableWriteStore.js';
19
19
  import type { Database } from './Database.js';
20
- import type { BootstrapData } from './sync/BootstrapFetcher.js';
21
20
  import type { WriteOptions } from './interfaces/index.js';
22
21
  import { LogPosition } from './logPosition.js';
23
- interface SyncObserver {
24
- onSync?: (event: SyncEvent) => void;
25
- }
26
- interface SyncEvent {
27
- type: 'create' | 'update' | 'delete' | 'archive' | 'rollback';
28
- modelType: string;
29
- model?: Model;
30
- modelId?: string;
31
- transactionType?: string;
32
- }
33
- interface SyncState {
34
- connectionState: 'connected' | 'disconnected' | 'connecting';
35
- pendingMutations: number;
36
- lastSyncAt?: Date;
37
- error?: Error;
38
- }
39
- export interface RehydrationStats {
40
- added: number;
41
- updated: number;
42
- removed: number;
43
- skipped: number;
44
- healed: number;
45
- elapsedMs: number;
46
- }
47
- /**
48
- * The slice of a bootstrap answer the pool applies: its rows, the models whose
49
- * server query failed, and the log position the snapshot was taken at — the
50
- * position every row in it reflects. `lastSyncId` is optional only for callers
51
- * applying rows with no snapshot position to speak of; the fetcher always
52
- * names one.
53
- */
54
- export type BootstrapSnapshot = Pick<BootstrapData, 'models' | 'failedModels'> & Partial<Pick<BootstrapData, 'lastSyncId'>>;
22
+ import { type RehydrationStats, type SyncObserver, type SyncState } from './syncClientTypes.js';
23
+ import type { BootstrapSnapshot } from './syncClientTypes.js';
24
+ export type { BootstrapSnapshot, RehydrationStats } from './syncClientTypes.js';
55
25
  export declare class SyncClient extends EventEmitter {
56
26
  private readonly runtime;
57
27
  private objectPool;
@@ -533,4 +503,3 @@ export declare class SyncClient extends EventEmitter {
533
503
  healed: number;
534
504
  };
535
505
  }
536
- export {};
@@ -24,22 +24,7 @@ import { LogPosition } from './logPosition.js';
24
24
  import { createLocalMutationPort } from './transactions/localMutation.js';
25
25
  import { createReconnectDrain } from './transactions/reconnectDrain.js';
26
26
  import { DatabaseCommitOutboxStore } from './transactions/databaseCommitOutbox.js';
27
- /**
28
- * Converts an untyped server `updatedAt` value — an ISO string, epoch number,
29
- * or Date read off an untyped row — into epoch milliseconds for
30
- * last-write-wins comparison. Falsy or non-date values become 0, matching the
31
- * conflict resolver's rule that a missing timestamp sorts as the epoch.
32
- */
33
- function toEpochMs(value) {
34
- if (!value)
35
- return 0;
36
- if (value instanceof Date)
37
- return value.getTime();
38
- if (typeof value === 'string' || typeof value === 'number') {
39
- return new Date(value).getTime();
40
- }
41
- return 0;
42
- }
27
+ import { toEpochMs, } from './syncClientTypes.js';
43
28
  export class SyncClient extends EventEmitter {
44
29
  runtime;
45
30
  objectPool;
@@ -10,6 +10,7 @@
10
10
  * The factory returns a plain object; the client assembles the `ablo.<model>`
11
11
  * lookup table from one of these per model.
12
12
  */
13
+ import type { CommitCreateOptions, CommitReceipt } from '@abloatai/transaction/resources/httpResources';
13
14
  import type { ModelTarget } from '@abloatai/transaction/coordination/schema';
14
15
  import type { ModelRegistry } from '../ModelRegistry.js';
15
16
  import type { InstanceCache } from '../InstanceCache.js';
@@ -17,7 +18,7 @@ import type { SyncClient } from '../SyncClient.js';
17
18
  import type { OnDemandLoader } from '../sync/OnDemandLoader.js';
18
19
  import type { JoinedParticipant } from '../sync/participants.js';
19
20
  import type { Duration, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease, ClaimWaitOptions, Snapshot } from '@abloatai/transaction/types/streams';
20
- export type { ModelListScope, ModelTrackParams, ModelTrackResult, LocalReadOptions, LocalCountOptions, ServerReadOptions, ServerGetOptions, ServerRetrieveOptions, ClaimTargetOptions, ClaimParams, ClaimContentionOptions, ClaimAttemptEvent, ClaimQueueView, ClaimSkipOptions, ClaimSkipParams, ClaimLookupParams, ClaimReorderParams, ClaimOptions, ClaimReadApi, AwaitedClaimMethod, ClaimApi, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, JoinOptions, } from '@abloatai/transaction/resources/modelOperations';
21
+ export type { ModelListScope, ModelTrackParams, ModelTrackResult, LocalReadOptions, LocalCountOptions, ServerReadOptions, ListAllOptions, ServerGetOptions, ServerRetrieveOptions, ClaimTargetOptions, ClaimParams, ClaimContentionOptions, ClaimAttemptEvent, ClaimQueueView, ClaimSkipOptions, ClaimSkipParams, ClaimLookupParams, ClaimReorderParams, ClaimOptions, ClaimReadApi, AwaitedClaimMethod, ClaimApi, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, JoinOptions, } from '@abloatai/transaction/resources/modelOperations';
21
22
  export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease };
22
23
  import type { ClaimApi, ClaimAttemptEvent, JoinOptions, LocalCountOptions, LocalReadOptions } from '@abloatai/transaction/resources/modelOperations';
23
24
  import type { HttpModelClient } from '@abloatai/transaction/transport/httpClient';
@@ -39,6 +40,17 @@ export interface ModelCollaboration {
39
40
  data: unknown;
40
41
  stamp: number;
41
42
  }>;
43
+ /**
44
+ * The batch commit lane, for a create handed a list of rows.
45
+ *
46
+ * A reactive client's single writes go through the mutation queue and land
47
+ * optimistically, because a rejected write rolls one row back. A batch
48
+ * cannot: it is atomic, so applying the rows one at a time would paint a
49
+ * half-written state the server may then decline whole. It goes down the
50
+ * same commit door the stateless client uses and the rows arrive on the
51
+ * ordinary stream, the way a teammate's would.
52
+ */
53
+ commitBatch(options: CommitCreateOptions): Promise<CommitReceipt>;
42
54
  createClaim(options: {
43
55
  /**
44
56
  * The locator, in the spelling the SDK surface and the HTTP routes use.
@@ -18,8 +18,9 @@ import { Model, modelAsRow } from '../Model.js';
18
18
  import { toMs } from '@abloatai/transaction/utils/duration';
19
19
  import { LEASE_TTL_MS } from '@abloatai/transaction/wire/protocol';
20
20
  import { heartbeatCadenceMs, resolveHeartbeatOptions, resolveHeartbeatPlan, startClaimHeartbeatLoop, } from '@abloatai/transaction/coordination/claimHeartbeatLoop';
21
- import { assertWriteOptions } from '@abloatai/transaction/resources/writeOptionsSchema';
22
- import { modelList } from '@abloatai/transaction/resources/httpResources';
21
+ import { assertWriteOptions, assertWriteTarget, } from '@abloatai/transaction/resources/writeOptionsSchema';
22
+ import { createModelId, resolveCreatedRows, resolveCreateId, } from '@abloatai/transaction/resources/modelCreate';
23
+ import { collectModelList, modelList, } from '@abloatai/transaction/resources/httpResources';
23
24
  import { subTarget } from '@abloatai/transaction/coordination';
24
25
  // A named claim-meta crossing (see `claim-meta-crossings-are-enumerated` in
25
26
  // .dependency-cruiser.cjs): the reactive proxy's self-claim targets are
@@ -685,6 +686,150 @@ hydration, collaboration, readSetContext) {
685
686
  }
686
687
  return page;
687
688
  });
689
+ /**
690
+ * Creates many rows as one atomic commit, and returns them in caller order.
691
+ */
692
+ const createManyRows = async (params) => {
693
+ if (params.data.length === 0)
694
+ return [];
695
+ if (!collaboration) {
696
+ throw new AbloValidationError(`Model "${schemaKey}" was built without the collaboration runtime, so a batch ` +
697
+ `create is unavailable here. Use the standard Ablo({ schema, apiKey }) client.`, { code: 'model_claim_not_configured' });
698
+ }
699
+ const prepared = prepareReadSet(readSetContext, readSetClientIdentity, undefined, undefined, params.idempotencyKey, params.reads);
700
+ try {
701
+ const organizationId = syncClient.getOrganizationId() ?? undefined;
702
+ const ids = [];
703
+ const operations = params.data.map((row) => {
704
+ const fields = row;
705
+ const id = resolveCreateId(undefined, fields) ??
706
+ createModelId(registeredModelName, params.idempotencyKey ? `${params.idempotencyKey}:${ids.length}` : null);
707
+ ids.push(id);
708
+ return {
709
+ action: 'create',
710
+ model: registeredModelName,
711
+ data: { organizationId: fields.organizationId ?? organizationId, ...fields, id },
712
+ id,
713
+ };
714
+ });
715
+ const receipt = await collaboration.commitBatch({
716
+ operations,
717
+ wait: 'confirmed',
718
+ ...(prepared.idempotencyKey
719
+ ? { idempotencyKey: prepared.idempotencyKey }
720
+ : params.idempotencyKey
721
+ ? { idempotencyKey: params.idempotencyKey }
722
+ : {}),
723
+ ...(prepared.reads
724
+ ? { reads: [...prepared.reads] }
725
+ : {}),
726
+ ...(params.track ? { track: [...params.track] } : {}),
727
+ });
728
+ const rows = await resolveCreatedRows({
729
+ modelName: registeredModelName,
730
+ ids,
731
+ operationResults: receipt.operationResults,
732
+ readRow: async (id) => {
733
+ const read = await collaboration.readPoint(registeredModelName, id);
734
+ return read.data;
735
+ },
736
+ });
737
+ consumeReadSet(readSetContext, readSetClientIdentity, prepared.consumed, prepared.automaticCommit);
738
+ return rows;
739
+ }
740
+ catch (error) {
741
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
742
+ throw error;
743
+ }
744
+ };
745
+ // `create` takes one row or a list of them. The list form is atomic and
746
+ // is therefore NOT applied optimistically: see `createManyRows`.
747
+ const createImpl = guardWrite(async (params) => {
748
+ if (Array.isArray(params.data)) {
749
+ return createManyRows(params);
750
+ }
751
+ const single = params;
752
+ const id = resolveCreateId(single.id, single.data) ?? Model.generateId();
753
+ const claim = single.claim;
754
+ let autoLease;
755
+ if (claim && !isClaimHandle(claim)) {
756
+ if (!collaboration) {
757
+ throw new AbloValidationError(`Model "${schemaKey}" was built without the collaboration runtime, so claim() is unavailable here. Claiming needs no per-model config — use the standard Ablo({ schema, apiKey }) client and every model is claimable.`, { code: 'model_claim_not_configured' });
758
+ }
759
+ // Write intent: enter the new row's entity scope before acquiring the
760
+ // create-claim so the holder's claim presence broadcasts to everyone
761
+ // already in this entity group (closing the subscribe-versus-broadcast
762
+ // race — see `takeClaim`). Released with the lease in the `finally`
763
+ // below. Awaited for broadcast ordering; still best-effort.
764
+ await collaboration.pinScope?.({ [schemaKey]: id });
765
+ const contention = resolveClaimContentionOptions(claim);
766
+ autoLease = await collaboration.createClaim({
767
+ target: {
768
+ model: wireModel,
769
+ id,
770
+ ...subTarget(claim, schemaKey),
771
+ },
772
+ description: claimDescription(claim, 'creating'),
773
+ ttl: claim.ttl,
774
+ queue: contention.wait,
775
+ maxQueueDepth: contention.maxDepth,
776
+ waitTimeoutMs: contention.timeoutMs,
777
+ signal: contention.signal,
778
+ onStatus: contention.onStatus,
779
+ });
780
+ }
781
+ // Default `organizationId` from the client's identity, matching the other
782
+ // write path — without this, a caller that omits it would create an
783
+ // org-unscoped row on one write path but not the other. An explicit value
784
+ // in `data` still wins via the spread.
785
+ const orgDefault = params.data.organizationId ??
786
+ syncClient.getOrganizationId();
787
+ const model = new ModelClass({
788
+ id,
789
+ ...(orgDefault != null ? { organizationId: orgDefault } : {}),
790
+ ...params.data,
791
+ createdAt: new Date(),
792
+ updatedAt: new Date(),
793
+ });
794
+ let prepared;
795
+ try {
796
+ const resolved = preparedMutation(single);
797
+ prepared = resolved.prepared;
798
+ const effective = {
799
+ ...resolved.options,
800
+ ...(autoLease
801
+ ? {
802
+ claimRef: { id: autoLease.id },
803
+ ...(autoLease.fenceToken !== undefined
804
+ ? { fenceToken: autoLease.fenceToken }
805
+ : {}),
806
+ }
807
+ : {}),
808
+ ...(isClaimHandle(claim)
809
+ ? {
810
+ claimRef: { id: claim.id },
811
+ ...(claim.fenceToken !== undefined
812
+ ? { fenceToken: claim.fenceToken }
813
+ : {}),
814
+ }
815
+ : {}),
816
+ };
817
+ syncClient.add(model, effective);
818
+ await waitForMutation(model);
819
+ consumeReadSet(readSetContext, readSetClientIdentity, prepared.consumed, prepared.automaticCommit);
820
+ return modelAsRow(model);
821
+ }
822
+ catch (error) {
823
+ abortReadSetCommit(readSetContext, prepared?.automaticCommit ?? false);
824
+ throw error;
825
+ }
826
+ finally {
827
+ await autoLease?.release?.().catch(() => { });
828
+ }
829
+ });
830
+ function createRows(params) {
831
+ return createImpl(params);
832
+ }
688
833
  const operations = {
689
834
  local,
690
835
  get,
@@ -692,85 +837,12 @@ hydration, collaboration, readSetContext) {
692
837
  // No automatic scope enrolment on bulk `list`: that would subscribe to an
693
838
  // unbounded set of rows' entity groups.
694
839
  list,
695
- create: guardWrite(async (params) => {
696
- const id = params.id ?? Model.generateId();
697
- const claim = params.claim;
698
- let autoLease;
699
- if (claim && !isClaimHandle(claim)) {
700
- if (!collaboration) {
701
- throw new AbloValidationError(`Model "${schemaKey}" was built without the collaboration runtime, so claim() is unavailable here. Claiming needs no per-model config — use the standard Ablo({ schema, apiKey }) client and every model is claimable.`, { code: 'model_claim_not_configured' });
702
- }
703
- // Write intent: enter the new row's entity scope before acquiring the
704
- // create-claim so the holder's claim presence broadcasts to everyone
705
- // already in this entity group (closing the subscribe-versus-broadcast
706
- // race — see `takeClaim`). Released with the lease in the `finally`
707
- // below. Awaited for broadcast ordering; still best-effort.
708
- await collaboration.pinScope?.({ [schemaKey]: id });
709
- const contention = resolveClaimContentionOptions(claim);
710
- autoLease = await collaboration.createClaim({
711
- target: {
712
- model: wireModel,
713
- id,
714
- ...subTarget(claim, schemaKey),
715
- },
716
- description: claimDescription(claim, 'creating'),
717
- ttl: claim.ttl,
718
- queue: contention.wait,
719
- maxQueueDepth: contention.maxDepth,
720
- waitTimeoutMs: contention.timeoutMs,
721
- signal: contention.signal,
722
- onStatus: contention.onStatus,
723
- });
724
- }
725
- // Default `organizationId` from the client's identity, matching the other
726
- // write path — without this, a caller that omits it would create an
727
- // org-unscoped row on one write path but not the other. An explicit value
728
- // in `data` still wins via the spread.
729
- const orgDefault = params.data.organizationId ??
730
- syncClient.getOrganizationId();
731
- const model = new ModelClass({
732
- id,
733
- ...(orgDefault != null ? { organizationId: orgDefault } : {}),
734
- ...params.data,
735
- createdAt: new Date(),
736
- updatedAt: new Date(),
737
- });
738
- let prepared;
739
- try {
740
- const resolved = preparedMutation(params);
741
- prepared = resolved.prepared;
742
- const effective = {
743
- ...resolved.options,
744
- ...(autoLease
745
- ? {
746
- claimRef: { id: autoLease.id },
747
- ...(autoLease.fenceToken !== undefined
748
- ? { fenceToken: autoLease.fenceToken }
749
- : {}),
750
- }
751
- : {}),
752
- ...(isClaimHandle(claim)
753
- ? {
754
- claimRef: { id: claim.id },
755
- ...(claim.fenceToken !== undefined
756
- ? { fenceToken: claim.fenceToken }
757
- : {}),
758
- }
759
- : {}),
760
- };
761
- syncClient.add(model, effective);
762
- await waitForMutation(model);
763
- consumeReadSet(readSetContext, readSetClientIdentity, prepared.consumed, prepared.automaticCommit);
764
- return modelAsRow(model);
765
- }
766
- catch (error) {
767
- abortReadSetCommit(readSetContext, prepared?.automaticCommit ?? false);
768
- throw error;
769
- }
770
- finally {
771
- await autoLease?.release?.().catch(() => { });
772
- }
840
+ listAll: guard(async (options = {}) => {
841
+ const { maxPages, signal, ...readOptions } = options;
842
+ signal?.throwIfAborted();
843
+ return collectModelList(await list(readOptions), { maxPages, signal });
773
844
  }),
845
+ create: createRows,
774
846
  // `update` is overloaded — classic `update({ id, data })` + functional
775
847
  // `update(id, current => next)`. The IIFE keeps the shared error-guard
776
848
  // wrapping while exposing the two public signatures (a plain `guard(...)`
@@ -839,6 +911,10 @@ hydration, collaboration, readSetContext) {
839
911
  });
840
912
  }
841
913
  const params = arg;
914
+ // Named before anything reads it. Without this the row lookup below
915
+ // reports `Entity not found: Model/undefined`, which sends the reader
916
+ // looking for a missing row rather than at the unaddressed write.
917
+ assertWriteTarget('update', registeredModelName, params.id);
842
918
  const autoClaim = params.claim && !isClaimHandle(params.claim) ? params.claim : null;
843
919
  if (autoClaim) {
844
920
  const handle = await takeClaim({ ...autoClaim, id: params.id });
@@ -912,6 +988,9 @@ hydration, collaboration, readSetContext) {
912
988
  return update;
913
989
  })(),
914
990
  delete: guardWrite(async (params) => {
991
+ // Before the idempotent "ensure absent" below can read this as a row that
992
+ // is simply not here. An unaddressed delete is a mistake, not an absence.
993
+ assertWriteTarget('delete', registeredModelName, params.id);
915
994
  const autoClaim = params.claim && !isClaimHandle(params.claim) ? params.claim : null;
916
995
  if (autoClaim) {
917
996
  const handle = await takeClaim({ ...autoClaim, id: params.id });
@@ -408,6 +408,9 @@ export function buildReactiveEngine(inputs) {
408
408
  const registeredModelName = modelDef.typename ?? schemaKey;
409
409
  modelProxies[schemaKey] = createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, modelRegistry, hydration, {
410
410
  createClaim: (claimOptions) => publicClaims.create(claimOptions),
411
+ // Lazily referenced: `commits` is declared below this loop, and this
412
+ // only runs when someone actually writes a batch.
413
+ commitBatch: (commitOptions) => commits.create(commitOptions),
411
414
  readPoint,
412
415
  createSnapshot: (modelKey, id) => createSnapshot({
413
416
  pool: objectPool,
@@ -502,13 +505,14 @@ export function buildReactiveEngine(inputs) {
502
505
  if (wait === 'queued') {
503
506
  return { id: clientTxId, status: 'queued' };
504
507
  }
505
- const { lastSyncId, notifications, missingIds } = await queue.waitForCommitReceipt(clientTxId);
508
+ const { lastSyncId, notifications, missingIds, operationResults } = await queue.waitForCommitReceipt(clientTxId);
506
509
  return {
507
510
  id: clientTxId,
508
511
  status: 'confirmed',
509
512
  lastSyncId,
510
513
  ...(notifications && notifications.length > 0 ? { notifications } : {}),
511
514
  ...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
515
+ ...(operationResults && operationResults.length > 0 ? { operationResults } : {}),
512
516
  };
513
517
  },
514
518
  async get({ id }) {
@@ -50,6 +50,7 @@ export function createDefaultMutationExecutor(getWs, readSetContext) {
50
50
  ...(receipt.correlationId ? { correlationId: receipt.correlationId } : {}),
51
51
  ...(receipt.notifications ? { notifications: receipt.notifications } : {}),
52
52
  ...(receipt.missingIds ? { missingIds: receipt.missingIds } : {}),
53
+ ...(receipt.operationResults ? { operationResults: receipt.operationResults } : {}),
53
54
  });
54
55
  }
55
56
  if (!ws.sendCommit) {
@@ -0,0 +1,41 @@
1
+ import type { Model } from './Model.js';
2
+ import type { BootstrapData } from './sync/BootstrapFetcher.js';
3
+ import type { QueuedMutation } from './transactions/mutations/MutationQueue.js';
4
+ import type { CommitTransaction } from './transactions/mutations/commitLane.js';
5
+ export interface SyncObserver {
6
+ onSync?: (event: SyncEvent) => void;
7
+ }
8
+ export interface SyncEvent {
9
+ type: 'create' | 'update' | 'delete' | 'archive' | 'rollback';
10
+ modelType: string;
11
+ model?: Model;
12
+ modelId?: string;
13
+ transactionType?: string;
14
+ }
15
+ export interface SyncState {
16
+ connectionState: 'connected' | 'disconnected' | 'connecting';
17
+ pendingMutations: number;
18
+ lastSyncAt?: Date;
19
+ error?: Error;
20
+ }
21
+ export interface RehydrationStats {
22
+ added: number;
23
+ updated: number;
24
+ removed: number;
25
+ skipped: number;
26
+ healed: number;
27
+ elapsedMs: number;
28
+ }
29
+ export type EventHandler = () => void;
30
+ /** The bootstrap fields applied to the local object pool. */
31
+ export type BootstrapSnapshot = Pick<BootstrapData, 'models' | 'failedModels'> & Partial<Pick<BootstrapData, 'lastSyncId'>>;
32
+ /** A completed queued mutation or explicit commit. */
33
+ export type CompletedTransaction = (Pick<QueuedMutation, 'id' | 'modelId' | 'syncIdNeededForCompletion'> & {
34
+ lastSyncId?: undefined;
35
+ operations?: undefined;
36
+ }) | (Pick<CommitTransaction, 'id' | 'lastSyncId' | 'operations'> & {
37
+ modelId?: undefined;
38
+ syncIdNeededForCompletion?: undefined;
39
+ });
40
+ /** Normalize an untyped server timestamp for last-write-wins comparison. */
41
+ export declare function toEpochMs(value: unknown): number;
@@ -0,0 +1,11 @@
1
+ /** Normalize an untyped server timestamp for last-write-wins comparison. */
2
+ export function toEpochMs(value) {
3
+ if (!value)
4
+ return 0;
5
+ if (value instanceof Date)
6
+ return value.getTime();
7
+ if (typeof value === 'string' || typeof value === 'number') {
8
+ return new Date(value).getTime();
9
+ }
10
+ return 0;
11
+ }
@@ -18,6 +18,7 @@ import type { RuntimeContext } from '../../RuntimeContext.js';
18
18
  import { type LogPositionPort } from '../../logPosition.js';
19
19
  import type { WriteOptions } from '../../interfaces/index.js';
20
20
  import type { StaleNotification, ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
21
+ import { type CommitOperationResult } from '@abloatai/transaction/wire/commit';
21
22
  import { type MutationInput, type QueuedMutation, type UserContext } from './commitPayload.js';
22
23
  import { type CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
23
24
  import type { DurableWriteStore } from './durableWriteStore.js';
@@ -373,6 +374,7 @@ export declare class MutationQueue extends EventEmitter {
373
374
  lastSyncId: number;
374
375
  notifications?: StaleNotification[];
375
376
  missingIds?: string[];
377
+ operationResults?: CommitOperationResult[];
376
378
  }>;
377
379
  private isReorderPayload;
378
380
  private isPermanentError;