@abloatai/humans 0.60.0 → 0.62.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 (99) hide show
  1. package/dist/Ablo.d.ts +4 -4
  2. package/dist/Ablo.js +2 -1
  3. package/dist/client.d.ts +7 -12
  4. package/dist/humans.d.ts +2 -2
  5. package/dist/humans.js +2 -6
  6. package/dist/local/BaseSyncedStore.d.ts +2 -4
  7. package/dist/local/BaseSyncedStore.js +5 -8
  8. package/dist/local/Model.js +46 -56
  9. package/dist/local/NetworkMonitor.js +2 -0
  10. package/dist/local/RuntimeContext.js +2 -0
  11. package/dist/local/SyncClient.d.ts +8 -32
  12. package/dist/local/SyncClient.js +26 -99
  13. package/dist/local/client/clientPrelude.d.ts +2 -0
  14. package/dist/local/client/clientPrelude.js +13 -1
  15. package/dist/local/client/createInternalComponents.js +2 -0
  16. package/dist/local/client/createModelOperations.d.ts +5 -0
  17. package/dist/local/client/createModelOperations.js +7 -4
  18. package/dist/local/client/reactiveEngine.d.ts +2 -2
  19. package/dist/local/client/reactiveEngine.js +9 -11
  20. package/dist/local/fileUploads.d.ts +27 -0
  21. package/dist/local/fileUploads.js +55 -0
  22. package/dist/local/query/client.d.ts +3 -0
  23. package/dist/local/query/client.js +1 -1
  24. package/dist/local/stores/syncAction.d.ts +1 -1
  25. package/dist/local/sync/BootstrapFetcher.d.ts +2 -0
  26. package/dist/local/sync/BootstrapFetcher.js +4 -4
  27. package/dist/local/sync/OnDemandLoader.d.ts +2 -0
  28. package/dist/local/sync/OnDemandLoader.js +1 -0
  29. package/dist/local/sync/SyncWebSocket.d.ts +2 -15
  30. package/dist/local/sync/SyncWebSocket.js +1 -36
  31. package/dist/local/sync/contextOnChange.js +1 -1
  32. package/dist/local/sync/createClaimStream.d.ts +9 -19
  33. package/dist/local/sync/createClaimStream.js +41 -56
  34. package/dist/local/sync/deltaPipeline.js +12 -6
  35. package/dist/local/sync/schemas.d.ts +2 -2
  36. package/dist/local/sync/socketEventWiring.d.ts +1 -2
  37. package/dist/local/sync/socketEventWiring.js +1 -5
  38. package/dist/local/transactions/localMutation.js +3 -3
  39. package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -5
  40. package/dist/local/transactions/mutations/MutationQueue.js +25 -51
  41. package/dist/local/transactions/mutations/batchProcessing.js +23 -10
  42. package/dist/local/transactions/mutations/commitPayload.d.ts +8 -1
  43. package/dist/local/transactions/mutations/commitTransport.js +3 -1
  44. package/dist/local/transactions/mutations/executionSelection.d.ts +0 -1
  45. package/dist/local/transactions/mutations/executionSelection.js +9 -17
  46. package/dist/local/transactions/mutations/failureHandling.js +9 -0
  47. package/dist/local/transactions/mutations/localMutation.js +3 -3
  48. package/dist/local/transactions/mutations/queueCoalescing.js +8 -0
  49. package/dist/local/transactions/mutations/replayValidation.d.ts +4 -4
  50. package/dist/presence/index.d.ts +15 -0
  51. package/dist/presence/index.js +49 -0
  52. package/dist/react/AbloProvider.d.ts +3 -3
  53. package/dist/react/AbloProvider.js +4 -3
  54. package/dist/react/useErrorListener.js +1 -1
  55. package/dist/react/useMutationFailureListener.js +1 -1
  56. package/dist/surface.d.ts +1 -1
  57. package/dist/surface.js +1 -0
  58. package/package.json +3 -4
  59. package/src/Ablo.ts +15 -6
  60. package/src/client.ts +8 -13
  61. package/src/humans.ts +3 -10
  62. package/src/local/BaseSyncedStore.ts +5 -11
  63. package/src/local/Model.ts +45 -55
  64. package/src/local/NetworkMonitor.ts +2 -0
  65. package/src/local/RuntimeContext.ts +2 -0
  66. package/src/local/SyncClient.ts +33 -127
  67. package/src/local/client/clientPrelude.ts +20 -0
  68. package/src/local/client/createInternalComponents.ts +2 -0
  69. package/src/local/client/createModelOperations.ts +17 -6
  70. package/src/local/client/reactiveEngine.ts +19 -14
  71. package/src/local/fileUploads.ts +97 -0
  72. package/src/local/query/client.ts +4 -0
  73. package/src/local/sync/BootstrapFetcher.ts +13 -4
  74. package/src/local/sync/OnDemandLoader.ts +3 -0
  75. package/src/local/sync/SyncWebSocket.ts +1 -42
  76. package/src/local/sync/contextOnChange.ts +1 -1
  77. package/src/local/sync/createClaimStream.ts +52 -72
  78. package/src/local/sync/deltaPipeline.ts +10 -6
  79. package/src/local/sync/socketEventWiring.ts +1 -8
  80. package/src/local/transactions/localMutation.ts +3 -3
  81. package/src/local/transactions/mutations/MutationQueue.ts +24 -53
  82. package/src/local/transactions/mutations/batchProcessing.ts +25 -10
  83. package/src/local/transactions/mutations/commitPayload.ts +11 -1
  84. package/src/local/transactions/mutations/commitTransport.ts +2 -2
  85. package/src/local/transactions/mutations/executionSelection.ts +9 -15
  86. package/src/local/transactions/mutations/failureHandling.ts +10 -0
  87. package/src/local/transactions/mutations/localMutation.ts +3 -3
  88. package/src/local/transactions/mutations/queueCoalescing.ts +6 -0
  89. package/src/presence/index.ts +72 -0
  90. package/src/react/AbloProvider.tsx +14 -9
  91. package/src/react/useErrorListener.ts +1 -1
  92. package/src/react/useMutationFailureListener.ts +1 -1
  93. package/src/surface.ts +1 -0
  94. package/dist/local/transactions/mutations/pendingDrain.d.ts +0 -33
  95. package/dist/local/transactions/mutations/pendingDrain.js +0 -117
  96. package/dist/presenceStream.d.ts +0 -69
  97. package/dist/presenceStream.js +0 -200
  98. package/src/local/transactions/mutations/pendingDrain.ts +0 -169
  99. package/src/presenceStream.ts +0 -279
package/dist/Ablo.d.ts CHANGED
@@ -57,7 +57,7 @@ export type Ablo<S extends SchemaRecord> = AbloClient<S>;
57
57
  * const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
58
58
  * ```
59
59
  *
60
- * Server-side agents, workers, and services use `@abloatai/transaction`.
60
+ * Server-side agents, workers, and services use `@abloatai/ablo`.
61
61
  */
62
62
  export declare function Ablo<const S extends SchemaRecord, const P extends readonly AbloPlugin[]>(options: AbloOptions<S> & {
63
63
  plugins: P;
@@ -97,10 +97,10 @@ export declare namespace Ablo {
97
97
  type ClaimTarget = _Streams.ClaimTarget;
98
98
  type PresenceTarget = _Streams.PresenceTarget;
99
99
  type Duration = _Streams.Duration;
100
- type PresenceStream = _Streams.PresenceStream;
100
+ type Presence = import('./presence/index.js').ReactivePresence;
101
+ type PresenceSession = import('@abloatai/transaction/presence').PresenceSession;
102
+ type PresenceActivity = import('@abloatai/transaction/presence').PresenceActivity;
101
103
  type ClaimStream = _Streams.ClaimStream;
102
- type Peer = _Streams.Peer;
103
- type Activity = _Streams.Activity;
104
104
  type Claim = _Streams.Claim;
105
105
  type ClaimRejection = _Streams.ClaimRejection;
106
106
  type ClaimLost = _Streams.ClaimLost;
package/dist/Ablo.js CHANGED
@@ -39,7 +39,7 @@ export function Ablo(options) {
39
39
  // resolver, the base URL, the logger, and this participant's identity —
40
40
  // and fails on a misconfiguration before anything is constructed.
41
41
  const prelude = resolveClientPrelude(options);
42
- const { internalOptions, authCredentials, logger, url, participantId, kind } = prelude;
42
+ const { internalOptions, authCredentials, presenceSession, logger, url, participantId, kind, } = prelude;
43
43
  // 2. The connection, built here in the composition root — before the plugin
44
44
  // list resolves, so `PluginContext.transport` carries the instance a
45
45
  // plugin holds for the client's lifetime. It holds no socket until
@@ -55,6 +55,7 @@ export function Ablo(options) {
55
55
  baseUrl: url,
56
56
  kind,
57
57
  getAuthToken: authCredentials.getAuthToken,
58
+ presenceSession,
58
59
  collaborationEvents: [...(internalOptions.collaborationEvents ?? [])],
59
60
  syncGroups: [...(internalOptions.syncGroups ?? [])],
60
61
  deferConnect: true,
package/dist/client.d.ts CHANGED
@@ -11,7 +11,6 @@
11
11
  * the factory back and creating a cycle.
12
12
  */
13
13
  import type { Schema, SchemaRecord, Model, InferCreate, InferRow } from '@abloatai/transaction/schema/schema';
14
- import type { PresenceStream } from '@abloatai/transaction/types/streams';
15
14
  import type { InstanceCache } from './local/InstanceCache.js';
16
15
  import type { SyncStoreContract } from './react/context.js';
17
16
  import type { SyncWebSocket, CoreSyncEventMap } from './local/sync/SyncWebSocket.js';
@@ -21,6 +20,7 @@ import type { ClaimResource, CommitResource } from '@abloatai/transaction/client
21
20
  import type { EffectiveAuthority } from '@abloatai/transaction/auth';
22
21
  import type { ReadDependency } from '@abloatai/transaction/coordination';
23
22
  import type { CapturedRow } from '@abloatai/transaction/transport/http';
23
+ import type { ReactivePresence } from './presence/index.js';
24
24
  export type { LocalReadOptions } from './local/client/resourceTypes.js';
25
25
  /** The typed sync engine client — one property per model in the schema */
26
26
  export type AbloClient<S extends SchemaRecord> = {
@@ -197,19 +197,14 @@ export type AbloClient<S extends SchemaRecord> = {
197
197
  * ```
198
198
  */
199
199
  readonly syncStatus: SyncStatus;
200
- /** The underlying schema */
201
- readonly schema: Schema<S>;
202
200
  /**
203
- * A real-time presence livestream — who else is connected on this engine's
204
- * sync groups, what they're doing, and a write surface for announcing this
205
- * user's own activity. It rides the engine's existing WebSocket; opening a
206
- * participant for presence does not open a second socket. See
207
- * {@link PresenceStream}.
208
- *
209
- * The reference is stable for the engine's lifetime — the underlying connection
210
- * is rotated on `dispose()`, but this object stays the same.
201
+ * Session-owned live activity projected from this client's existing
202
+ * connection. Use `active` for every visible activity, `others` to exclude
203
+ * this session, and `forModel(model, id?)` for a model-native view.
211
204
  */
212
- readonly presence: PresenceStream;
205
+ readonly presence: ReactivePresence;
206
+ /** The underlying schema */
207
+ readonly schema: Schema<S>;
213
208
  /**
214
209
  * @internal The supported coordination API is `ablo.<model>.claim`. This
215
210
  * accessor is the internal stream that surface is built on and is not part of
package/dist/humans.d.ts CHANGED
@@ -6,10 +6,10 @@
6
6
  * contracts.
7
7
  */
8
8
  import type { PluginContext, AppliedChange } from './plugin.js';
9
- import { type AttachablePresenceStream } from './presenceStream.js';
9
+ import { type AttachablePresence } from './presence/index.js';
10
10
  import { kStoreCluster, type InternalAbloOptions, type StoreCluster } from './local/client/storeCluster.js';
11
11
  export interface HumansSurface {
12
- readonly presence: AttachablePresenceStream;
12
+ readonly presence: AttachablePresence;
13
13
  readonly [kStoreCluster]?: StoreCluster;
14
14
  }
15
15
  export declare function humans(): {
package/dist/humans.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AbloValidationError } from '@abloatai/transaction/errors';
2
- import { createPresenceStream } from './presenceStream.js';
2
+ import { createPresence } from './presence/index.js';
3
3
  import { buildStoreCluster, kStoreCluster, } from './local/client/storeCluster.js';
4
4
  export function humans() {
5
5
  let applyChanges = null;
@@ -22,11 +22,7 @@ export function humans() {
22
22
  applyChanges = (changes) => { cluster.store.applyChangesToPool(changes); };
23
23
  }
24
24
  return {
25
- presence: createPresenceStream({
26
- participantId: context.participant?.id ?? '',
27
- syncGroups: [...(context.syncGroups ?? [])],
28
- isAgent: context.participant?.kind === 'agent',
29
- }, context.transport ?? null),
25
+ presence: createPresence(context.transport ?? null),
30
26
  ...(cluster ? { [kStoreCluster]: cluster } : {}),
31
27
  };
32
28
  },
@@ -18,7 +18,7 @@ import type { SyncClient } from './SyncClient.js';
18
18
  import type { Database, BootstrapResult, BootstrapRequirements } from './Database.js';
19
19
  import type { InstanceCache } from './InstanceCache.js';
20
20
  import { ModelRegistry } from './ModelRegistry.js';
21
- import { SyncWebSocket, type SyncDelta, type SyncGroupChangePayload, type GroupAddedPayload, type GroupRemovedPayload, type BootstrapHint, type BootstrapDataEvent, type PresenceUpdate, type EventMap, type DefaultCollaborationEvents, type SyncWebSocketEventMap } from './sync/SyncWebSocket.js';
21
+ import { SyncWebSocket, type SyncDelta, type SyncGroupChangePayload, type GroupAddedPayload, type GroupRemovedPayload, type BootstrapHint, type BootstrapDataEvent, type EventMap, type DefaultCollaborationEvents, type SyncWebSocketEventMap } from './sync/SyncWebSocket.js';
22
22
  import { QueryProcessor } from './query/QueryProcessor.js';
23
23
  import { Model } from './Model.js';
24
24
  import type { RuntimeContext } from './RuntimeContext.js';
@@ -153,7 +153,7 @@ export declare const BOOTSTRAP_CONFIG: {
153
153
  readonly RETRY_DELAY_MS: 500;
154
154
  };
155
155
  export { ModelScope };
156
- export type { SyncDelta, SyncGroupChangePayload, GroupAddedPayload, GroupRemovedPayload, BootstrapHint, BootstrapDataEvent, PresenceUpdate, };
156
+ export type { SyncDelta, SyncGroupChangePayload, GroupAddedPayload, GroupRemovedPayload, BootstrapHint, BootstrapDataEvent, };
157
157
  export { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
158
158
  /**
159
159
  * The abstract base class that application-specific sync stores extend. It
@@ -791,8 +791,6 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
791
791
  protected handleBootstrapRequired(_hint: BootstrapHint): void;
792
792
  /** Handle bootstrap_data event. Override in subclass. */
793
793
  protected handleBootstrapData(_data: BootstrapDataEvent): void;
794
- /** Handle presence_update event. Override in subclass. */
795
- protected handlePresenceUpdate(_data: PresenceUpdate): void;
796
794
  protected incrementPendingChanges(): void;
797
795
  protected decrementPendingChanges(): void;
798
796
  protected updateSyncStatus(updates: Partial<SyncStatus>): void;
@@ -1105,7 +1105,6 @@ export class BaseSyncedStore {
1105
1105
  applyDeltaFrame: (deltas) => { this.applyDeltaFrame(deltas); },
1106
1106
  handleBootstrapRequired: (hint) => { this.handleBootstrapRequired(hint); },
1107
1107
  handleBootstrapData: (data) => { this.handleBootstrapData(data); },
1108
- handlePresenceUpdate: (data) => { this.handlePresenceUpdate(data); },
1109
1108
  performCredentialRefresh: () => this.performCredentialRefresh(),
1110
1109
  handleTerminalSessionError: (error) => { this.terminalSessionLifecycle.start(error); },
1111
1110
  nudgeReconnect: () => { this.nudgeReconnect(); },
@@ -1334,10 +1333,10 @@ export class BaseSyncedStore {
1334
1333
  const isCreate = !this.objectPool.get(model.id);
1335
1334
  if (isCreate) {
1336
1335
  model.updatedAt = new Date();
1337
- this.syncClient.add(model);
1336
+ await this.syncClient.add(model);
1338
1337
  }
1339
1338
  else {
1340
- this.syncClient.update(model);
1339
+ await this.syncClient.update(model);
1341
1340
  }
1342
1341
  }
1343
1342
  /** Save with an atomic server mutation (e.g., createSectionWithBlocks) */
@@ -1350,19 +1349,19 @@ export class BaseSyncedStore {
1350
1349
  const model = rowAsModel(entity);
1351
1350
  this.pendingDeletes.add(model.id);
1352
1351
  // SyncClient.delete handles: pool remove, transaction queue
1353
- this.syncClient.delete(model);
1352
+ await this.syncClient.delete(model);
1354
1353
  }
1355
1354
  /** Archive a model. Accepts schema-inferred entity shapes (see `save`). */
1356
1355
  async archive(entity) {
1357
1356
  const model = rowAsModel(entity);
1358
1357
  model.archivedAt = new Date();
1359
- this.syncClient.archive(model);
1358
+ await this.syncClient.archive(model);
1360
1359
  }
1361
1360
  /** Unarchive a model. Accepts schema-inferred entity shapes (see `save`). */
1362
1361
  async unarchive(entity) {
1363
1362
  const model = rowAsModel(entity);
1364
1363
  model.archivedAt = null;
1365
- this.syncClient.update(model);
1364
+ await this.syncClient.update(model);
1366
1365
  }
1367
1366
  // ── Query API ────────────────────────────────────────────────────────────
1368
1367
  // `ablo.<model>.local.get` / `.local.list` is the read surface for
@@ -1466,8 +1465,6 @@ export class BaseSyncedStore {
1466
1465
  handleBootstrapData(_data) {
1467
1466
  this.updateSyncStatus({ state: 'syncing' });
1468
1467
  }
1469
- /** Handle presence_update event. Override in subclass. */
1470
- handlePresenceUpdate(_data) { }
1471
1468
  // ── Pending changes tracking ─────────────────────────────────────────────
1472
1469
  incrementPendingChanges() {
1473
1470
  runInAction(() => { this.syncStatus.pendingChanges++; });
@@ -311,7 +311,7 @@ export class Model {
311
311
  for (const key of keys) {
312
312
  if (key === 'id')
313
313
  continue;
314
- const mod = modified?.get(key);
314
+ const mod = modified.get(key);
315
315
  if (mod) {
316
316
  out[key] = mod.old;
317
317
  }
@@ -358,25 +358,23 @@ export class Model {
358
358
  const errors = [];
359
359
  const modelName = this.getModelName();
360
360
  const properties = getActiveRegistry().getProperties(modelName);
361
- if (properties) {
362
- const json = this.toJSON();
363
- for (const [propName, metadata] of properties) {
364
- // Check required fields
365
- if (!metadata.nullable && !metadata.optional) {
366
- const value = json[propName];
367
- if (value == null || value === '') {
368
- errors.push(`${propName} is required`);
369
- }
361
+ const json = this.toJSON();
362
+ for (const [propName, metadata] of properties) {
363
+ // Check required fields
364
+ if (!metadata.nullable && !metadata.optional) {
365
+ const value = json[propName];
366
+ if (value == null || value === '') {
367
+ errors.push(`${propName} is required`);
370
368
  }
371
- // Run custom validation rules
372
- const rules = this.validationRules[propName];
373
- if (rules) {
374
- const value = json[propName];
375
- for (const rule of rules) {
376
- const error = rule(value);
377
- if (error)
378
- errors.push(error);
379
- }
369
+ }
370
+ // Run custom validation rules
371
+ const rules = this.validationRules[propName];
372
+ if (rules) {
373
+ const value = json[propName];
374
+ for (const rule of rules) {
375
+ const error = rule(value);
376
+ if (error)
377
+ errors.push(error);
380
378
  }
381
379
  }
382
380
  }
@@ -679,20 +677,18 @@ export class Model {
679
677
  if (this.archivedAt !== undefined) {
680
678
  result.archivedAt = this.archivedAt?.toISOString() ?? null;
681
679
  }
682
- if (properties) {
683
- const self = this;
684
- for (const [propName, metadata] of properties) {
685
- // Skip certain types
686
- if (metadata.type === 'ephemeralProperty')
687
- continue;
688
- if (metadata.type === 'referenceModel')
689
- continue;
690
- if (metadata.type === 'referenceCollection')
691
- continue;
692
- const value = self[propName];
693
- if (value !== undefined) {
694
- result[propName] = value;
695
- }
680
+ const self = this;
681
+ for (const [propName, metadata] of properties) {
682
+ // Skip certain types
683
+ if (metadata.type === 'ephemeralProperty')
684
+ continue;
685
+ if (metadata.type === 'referenceModel')
686
+ continue;
687
+ if (metadata.type === 'referenceCollection')
688
+ continue;
689
+ const value = self[propName];
690
+ if (value !== undefined) {
691
+ result[propName] = value;
696
692
  }
697
693
  }
698
694
  return result;
@@ -793,14 +789,12 @@ export class Model {
793
789
  if (hasActiveRegistry()) {
794
790
  const modelName = this.getModelName();
795
791
  const properties = getActiveRegistry().getProperties(modelName);
796
- if (properties) {
797
- const self = this;
798
- for (const [propName, metadata] of properties) {
799
- if (metadata.type === 'referenceCollection') {
800
- const collection = self[propName];
801
- if (collection?.dispose) {
802
- collection.dispose();
803
- }
792
+ const self = this;
793
+ for (const [propName, metadata] of properties) {
794
+ if (metadata.type === 'referenceCollection') {
795
+ const collection = self[propName];
796
+ if (collection) {
797
+ collection.dispose();
804
798
  }
805
799
  }
806
800
  }
@@ -825,11 +819,9 @@ export class Model {
825
819
  const snapshot = {};
826
820
  const modelName = this.getModelName();
827
821
  const properties = getActiveRegistry().getProperties(modelName);
828
- if (properties) {
829
- const json = this.toJSON();
830
- for (const [propName] of properties) {
831
- snapshot[propName] = json[propName];
832
- }
822
+ const json = this.toJSON();
823
+ for (const [propName] of properties) {
824
+ snapshot[propName] = json[propName];
833
825
  }
834
826
  return snapshot;
835
827
  }
@@ -874,17 +866,15 @@ export class Model {
874
866
  snapshot.archivedAt = this.archivedAt;
875
867
  const properties = getActiveRegistry().getProperties(this.getModelName());
876
868
  const self = this;
877
- if (properties) {
878
- for (const [propName, metadata] of properties) {
879
- if (metadata.type === 'ephemeralProperty' ||
880
- metadata.type === 'referenceModel' ||
881
- metadata.type === 'referenceCollection') {
882
- continue;
883
- }
884
- // Reading through the observable getter is the point: it subscribes the
885
- // enclosing MobX reaction to this field.
886
- snapshot[propName] = self[propName];
869
+ for (const [propName, metadata] of properties) {
870
+ if (metadata.type === 'ephemeralProperty' ||
871
+ metadata.type === 'referenceModel' ||
872
+ metadata.type === 'referenceCollection') {
873
+ continue;
887
874
  }
875
+ // Reading through the observable getter is the point: it subscribes the
876
+ // enclosing MobX reaction to this field.
877
+ snapshot[propName] = self[propName];
888
878
  }
889
879
  for (const name of this.getDerivedGetterNames()) {
890
880
  Object.defineProperty(snapshot, name, {
@@ -12,6 +12,8 @@ export class NetworkMonitor extends EventEmitter {
12
12
  // Only `navigator.onLine === false` means offline. Node 18+ exposes a global
13
13
  // `navigator` with `onLine === undefined`, so the naive `navigator.onLine`
14
14
  // would seed `false` (offline) on every server client — start optimistic.
15
+ // DOM types say `onLine` is boolean, but Node exposes it as undefined.
16
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
15
17
  isOnline = !(typeof navigator !== 'undefined' && navigator.onLine === false);
16
18
  lastOnlineCheck = new Date();
17
19
  constructor(runtime = globalRuntime) {
@@ -47,6 +47,8 @@ export const browserOnlineStatus = {
47
47
  // signal. Don't use `!navigator.onLine`: Node 18+ exposes a global
48
48
  // `navigator` whose `onLine` is `undefined`, which `!` would read as offline —
49
49
  // wedging every Node/server client (agents, worker, MCP) into a false offline.
50
+ // DOM types say `onLine` is boolean, but Node exposes it as undefined.
51
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
50
52
  return !(typeof navigator !== 'undefined' && navigator.onLine === false);
51
53
  },
52
54
  };
@@ -21,6 +21,7 @@ import type { WriteOptions } from './interfaces/index.js';
21
21
  import { LogPosition } from './logPosition.js';
22
22
  import { type RehydrationStats, type SyncObserver, type SyncState } from './syncClientTypes.js';
23
23
  import type { BootstrapSnapshot } from './syncClientTypes.js';
24
+ import { type BatchFileUploadOptions, type FileUploadOptions } from './fileUploads.js';
24
25
  export type { BootstrapSnapshot, RehydrationStats } from './syncClientTypes.js';
25
26
  export declare class SyncClient extends EventEmitter {
26
27
  private readonly runtime;
@@ -201,36 +202,11 @@ export declare class SyncClient extends EventEmitter {
201
202
  get gql(): import("./interfaces/index.js").MutationExecutor;
202
203
  /** Delete model (DELETE) - works offline */
203
204
  delete(model: Model, options?: WriteOptions): Promise<void> | undefined;
204
- /**
205
- * Upload a file and create its attachment record. The upload runs through
206
- * the {@link MutationQueue}, and a model is built from the server's
207
- * response and added to the pool.
208
- */
209
- uploadFile(file: File, options: {
210
- id: string;
211
- attachableType: string;
212
- attachableId: string;
213
- metadata?: Record<string, unknown>;
214
- }): Promise<Model | null>;
215
- /**
216
- * Batch upload files — single GraphQL call + parallel S3 PUTs.
217
- *
218
- * Returns the raw `Model[]` built by the object pool (typename is
219
- * determined by the payload the server returns — currently always
220
- * `Attachment`). The SDK has no knowledge of app-specific model classes,
221
- * so it cannot honestly claim a narrower return type; consumers that
222
- * need an `Attachment[]` project through their own typed accessor
223
- * (e.g. `store.query.attachments.findMany({ where: { id: IN ids } })`)
224
- * after the upload resolves.
225
- */
226
- batchUploadFiles(files: File[], options: {
227
- ids: string[];
228
- attachableType: string;
229
- attachableId: string;
230
- metadata?: Record<string, unknown>;
231
- }): Promise<Model[]>;
205
+ private fileUploadContext;
206
+ uploadFile(file: File, options: FileUploadOptions): Promise<Model | null>;
207
+ batchUploadFiles(files: File[], options: BatchFileUploadOptions): Promise<Model[]>;
232
208
  /** Archive model (ARCHIVE) - works offline */
233
- archive(model: Model): void;
209
+ archive(model: Model): Promise<void> | undefined;
234
210
  /**
235
211
  * Append a mutation to the pending queue and schedule its sync work.
236
212
  *
@@ -416,7 +392,7 @@ export declare class SyncClient extends EventEmitter {
416
392
  awaitingDeltaCount: number;
417
393
  awaitingDeltaTransactions: {
418
394
  id: string;
419
- type: "update" | "create" | "delete" | "archive" | "unarchive";
395
+ type: "create" | "update" | "delete" | "archive" | "unarchive";
420
396
  modelName: string;
421
397
  modelId: string;
422
398
  syncIdNeeded: number | undefined;
@@ -425,13 +401,13 @@ export declare class SyncClient extends EventEmitter {
425
401
  }[];
426
402
  pendingTransactions: {
427
403
  id: string;
428
- type: "update" | "create" | "delete" | "archive" | "unarchive";
404
+ type: "create" | "update" | "delete" | "archive" | "unarchive";
429
405
  modelName: string;
430
406
  modelId: string;
431
407
  }[];
432
408
  executingTransactions: {
433
409
  id: string;
434
- type: "update" | "create" | "delete" | "archive" | "unarchive";
410
+ type: "create" | "update" | "delete" | "archive" | "unarchive";
435
411
  modelName: string;
436
412
  modelId: string;
437
413
  }[];
@@ -14,7 +14,7 @@ import { deepEqual, snapshotJsonValue } from '@abloatai/transaction/utils/json';
14
14
  // ModelRegistry instance accessed via this.objectPool.registry
15
15
  import { LoadStrategy } from '@abloatai/transaction/types';
16
16
  import { globalRuntime } from './context.js';
17
- import { AbloAuthenticationError, AbloError, AbloValidationError } from '@abloatai/transaction/errors';
17
+ import { AbloError, AbloValidationError } from '@abloatai/transaction/errors';
18
18
  import { EventEmitter } from 'events';
19
19
  import { NetworkMonitor } from './NetworkMonitor.js';
20
20
  import { MutationQueue, } from './transactions/mutations/MutationQueue.js';
@@ -25,6 +25,8 @@ import { createLocalMutationPort } from './transactions/localMutation.js';
25
25
  import { createReconnectDrain } from './transactions/reconnectDrain.js';
26
26
  import { DatabaseCommitOutboxStore } from './transactions/databaseCommitOutbox.js';
27
27
  import { toEpochMs, } from './syncClientTypes.js';
28
+ import { batchUploadFiles, uploadFile, } from './fileUploads.js';
29
+ const ignoreSeparatelyObservedMutationFailure = () => undefined;
28
30
  export class SyncClient extends EventEmitter {
29
31
  runtime;
30
32
  objectPool;
@@ -856,7 +858,7 @@ export class SyncClient extends EventEmitter {
856
858
  if (capturedChanges === undefined)
857
859
  return;
858
860
  this.objectPool.upsert(model, ModelScope.live);
859
- this.stageMutation('update', model, capturedChanges);
861
+ void this.stageMutation('update', model, capturedChanges);
860
862
  this.notifyObservers({
861
863
  type: 'update',
862
864
  modelType: model.getModelName(),
@@ -876,99 +878,27 @@ export class SyncClient extends EventEmitter {
876
878
  this.mutationQueue.cancelTransactionsForModel(model.id);
877
879
  return this.mutate('delete', model, () => this.objectPool.remove(model.id), options);
878
880
  }
879
- /**
880
- * Upload a file and create its attachment record. The upload runs through
881
- * the {@link MutationQueue}, and a model is built from the server's
882
- * response and added to the pool.
883
- */
884
- async uploadFile(file, options) {
885
- if (!this.userId || !this.organizationId) {
886
- throw new AbloAuthenticationError('Authentication required for file uploads', {
887
- code: 'file_upload_auth_required',
888
- });
889
- }
890
- try {
891
- // Use MutationQueue to handle the upload mutation
892
- const result = await this.mutationQueue.uploadAttachment(file, {
893
- id: options.id,
894
- attachableType: options.attachableType,
895
- attachableId: options.attachableId,
896
- metadata: options.metadata,
897
- }, {
898
- userId: this.userId,
899
- organizationId: this.organizationId,
900
- });
901
- if (result) {
902
- // Create model from response using ModelRegistry (generic — no concrete class import)
903
- const model = this.objectPool.createFromData({
904
- id: options.id,
905
- ...result,
906
- });
907
- if (model) {
908
- this.objectPool.add(model, ModelScope.live);
909
- this.notifyObservers({
910
- type: 'create',
911
- modelType: model.getModelName(),
912
- model,
913
- });
914
- return model;
915
- }
916
- }
917
- return null;
918
- }
919
- catch (error) {
920
- this.runtime.observability.captureMutationFailure({
921
- context: 'file-upload',
922
- error: error instanceof Error ? error : new Error(String(error)),
923
- });
924
- throw error;
925
- }
926
- }
927
- /**
928
- * Batch upload files — single GraphQL call + parallel S3 PUTs.
929
- *
930
- * Returns the raw `Model[]` built by the object pool (typename is
931
- * determined by the payload the server returns — currently always
932
- * `Attachment`). The SDK has no knowledge of app-specific model classes,
933
- * so it cannot honestly claim a narrower return type; consumers that
934
- * need an `Attachment[]` project through their own typed accessor
935
- * (e.g. `store.query.attachments.findMany({ where: { id: IN ids } })`)
936
- * after the upload resolves.
937
- */
938
- async batchUploadFiles(files, options) {
939
- if (!this.userId || !this.organizationId) {
940
- throw new AbloAuthenticationError('Authentication required for file uploads', {
941
- code: 'file_upload_auth_required',
942
- });
943
- }
944
- const items = options.ids.map((id) => ({
945
- id,
946
- attachableType: options.attachableType,
947
- attachableId: options.attachableId,
948
- metadata: options.metadata,
949
- }));
950
- const results = await this.mutationQueue.batchUploadAttachments(files, items, {
881
+ fileUploadContext() {
882
+ return {
951
883
  userId: this.userId,
952
884
  organizationId: this.organizationId,
953
- });
954
- const models = [];
955
- for (const result of results) {
956
- const model = this.objectPool.createFromData({ ...result });
957
- if (model) {
958
- this.objectPool.add(model, ModelScope.live);
959
- this.notifyObservers({
960
- type: 'create',
961
- modelType: model.getModelName(),
962
- model,
963
- });
964
- models.push(model);
965
- }
966
- }
967
- return models;
885
+ mutationQueue: this.mutationQueue,
886
+ objectPool: this.objectPool,
887
+ observability: this.runtime.observability,
888
+ notifyCreated: (model) => {
889
+ this.notifyObservers({ type: 'create', modelType: model.getModelName(), model });
890
+ },
891
+ };
892
+ }
893
+ uploadFile(file, options) {
894
+ return uploadFile(this.fileUploadContext(), file, options);
895
+ }
896
+ batchUploadFiles(files, options) {
897
+ return batchUploadFiles(this.fileUploadContext(), files, options);
968
898
  }
969
899
  /** Archive model (ARCHIVE) - works offline */
970
900
  archive(model) {
971
- this.mutate('archive', model, () => { this.objectPool.updateScope(model.id, ModelScope.archived); });
901
+ return this.mutate('archive', model, () => { this.objectPool.updateScope(model.id, ModelScope.archived); });
972
902
  }
973
903
  /**
974
904
  * Append a mutation to the pending queue and schedule its sync work.
@@ -999,7 +929,7 @@ export class SyncClient extends EventEmitter {
999
929
  // Most internal callers intentionally use fire-and-forget writes. Observe
1000
930
  // their rejection without replacing the exact promise returned to model
1001
931
  // operations that need authoritative per-transaction confirmation.
1002
- void confirmation.catch(() => undefined);
932
+ void confirmation.catch(ignoreSeparatelyObservedMutationFailure);
1003
933
  const pending = staging.then(() => undefined).catch((error) => {
1004
934
  this.runtime.observability.captureMutationFailure({
1005
935
  context: `stage-mutation-${type}`,
@@ -1225,14 +1155,11 @@ export class SyncClient extends EventEmitter {
1225
1155
  markConnected() {
1226
1156
  this.setConnectionState('connected');
1227
1157
  // Browser online state may have marked the client connected before the
1228
- // WebSocket itself was ready. Always kick both durable lanes on the real
1229
- // socket event, even when the high-level state did not change.
1230
- void this.drainPendingConfirmations().catch((error) => {
1231
- this.runtime.observability.captureMutationFailure({
1232
- context: 'restore-commit-outbox',
1233
- error: error instanceof Error ? error : new Error(String(error)),
1234
- });
1235
- });
1158
+ // WebSocket itself was ready. Kick the durable lanes through the staging
1159
+ // barrier: a model mutation enters the in-memory store before its journal
1160
+ // row finishes saving, so a direct reconnect drain can otherwise try to
1161
+ // seal a source record that does not exist yet. The pending drain also
1162
+ // starts the atomic commit lane, so one ordered entry point covers both.
1236
1163
  void this.processPendingMutations();
1237
1164
  }
1238
1165
  drainPendingConfirmations() {
@@ -15,6 +15,7 @@ import type { ParticipantKind } from '@abloatai/transaction/types/participant';
15
15
  import type { Logger } from '@abloatai/transaction/logger';
16
16
  import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
17
17
  import { type AuthCredentialSource } from '@abloatai/transaction/auth/credentialSource';
18
+ import { type PresenceSessionSource } from '@abloatai/transaction/presence';
18
19
  import { type CredentialProvider } from '@abloatai/transaction/auth/apiKey';
19
20
  import type { AbloOptions, InternalAbloOptions } from './options.js';
20
21
  /** What one pass over the options bag settles, for the builders downstream. */
@@ -31,6 +32,7 @@ export interface ClientPrelude<S extends SchemaRecord> {
31
32
  */
32
33
  readonly credentialResolver: CredentialProvider | null;
33
34
  readonly authCredentials: AuthCredentialSource;
35
+ readonly presenceSession: PresenceSessionSource;
34
36
  readonly logger: Logger;
35
37
  readonly url: string;
36
38
  /**