@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.
- package/dist/Ablo.d.ts +4 -4
- package/dist/Ablo.js +2 -1
- package/dist/client.d.ts +7 -12
- package/dist/humans.d.ts +2 -2
- package/dist/humans.js +2 -6
- package/dist/local/BaseSyncedStore.d.ts +2 -4
- package/dist/local/BaseSyncedStore.js +5 -8
- package/dist/local/Model.js +46 -56
- package/dist/local/NetworkMonitor.js +2 -0
- package/dist/local/RuntimeContext.js +2 -0
- package/dist/local/SyncClient.d.ts +8 -32
- package/dist/local/SyncClient.js +26 -99
- package/dist/local/client/clientPrelude.d.ts +2 -0
- package/dist/local/client/clientPrelude.js +13 -1
- package/dist/local/client/createInternalComponents.js +2 -0
- package/dist/local/client/createModelOperations.d.ts +5 -0
- package/dist/local/client/createModelOperations.js +7 -4
- package/dist/local/client/reactiveEngine.d.ts +2 -2
- package/dist/local/client/reactiveEngine.js +9 -11
- package/dist/local/fileUploads.d.ts +27 -0
- package/dist/local/fileUploads.js +55 -0
- package/dist/local/query/client.d.ts +3 -0
- package/dist/local/query/client.js +1 -1
- package/dist/local/stores/syncAction.d.ts +1 -1
- package/dist/local/sync/BootstrapFetcher.d.ts +2 -0
- package/dist/local/sync/BootstrapFetcher.js +4 -4
- package/dist/local/sync/OnDemandLoader.d.ts +2 -0
- package/dist/local/sync/OnDemandLoader.js +1 -0
- package/dist/local/sync/SyncWebSocket.d.ts +2 -15
- package/dist/local/sync/SyncWebSocket.js +1 -36
- package/dist/local/sync/contextOnChange.js +1 -1
- package/dist/local/sync/createClaimStream.d.ts +9 -19
- package/dist/local/sync/createClaimStream.js +41 -56
- package/dist/local/sync/deltaPipeline.js +12 -6
- package/dist/local/sync/schemas.d.ts +2 -2
- package/dist/local/sync/socketEventWiring.d.ts +1 -2
- package/dist/local/sync/socketEventWiring.js +1 -5
- package/dist/local/transactions/localMutation.js +3 -3
- package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -5
- package/dist/local/transactions/mutations/MutationQueue.js +25 -51
- package/dist/local/transactions/mutations/batchProcessing.js +23 -10
- package/dist/local/transactions/mutations/commitPayload.d.ts +8 -1
- package/dist/local/transactions/mutations/commitTransport.js +3 -1
- package/dist/local/transactions/mutations/executionSelection.d.ts +0 -1
- package/dist/local/transactions/mutations/executionSelection.js +9 -17
- package/dist/local/transactions/mutations/failureHandling.js +9 -0
- package/dist/local/transactions/mutations/localMutation.js +3 -3
- package/dist/local/transactions/mutations/queueCoalescing.js +8 -0
- package/dist/local/transactions/mutations/replayValidation.d.ts +4 -4
- package/dist/presence/index.d.ts +15 -0
- package/dist/presence/index.js +49 -0
- package/dist/react/AbloProvider.d.ts +3 -3
- package/dist/react/AbloProvider.js +4 -3
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/package.json +3 -4
- package/src/Ablo.ts +15 -6
- package/src/client.ts +8 -13
- package/src/humans.ts +3 -10
- package/src/local/BaseSyncedStore.ts +5 -11
- package/src/local/Model.ts +45 -55
- package/src/local/NetworkMonitor.ts +2 -0
- package/src/local/RuntimeContext.ts +2 -0
- package/src/local/SyncClient.ts +33 -127
- package/src/local/client/clientPrelude.ts +20 -0
- package/src/local/client/createInternalComponents.ts +2 -0
- package/src/local/client/createModelOperations.ts +17 -6
- package/src/local/client/reactiveEngine.ts +19 -14
- package/src/local/fileUploads.ts +97 -0
- package/src/local/query/client.ts +4 -0
- package/src/local/sync/BootstrapFetcher.ts +13 -4
- package/src/local/sync/OnDemandLoader.ts +3 -0
- package/src/local/sync/SyncWebSocket.ts +1 -42
- package/src/local/sync/contextOnChange.ts +1 -1
- package/src/local/sync/createClaimStream.ts +52 -72
- package/src/local/sync/deltaPipeline.ts +10 -6
- package/src/local/sync/socketEventWiring.ts +1 -8
- package/src/local/transactions/localMutation.ts +3 -3
- package/src/local/transactions/mutations/MutationQueue.ts +24 -53
- package/src/local/transactions/mutations/batchProcessing.ts +25 -10
- package/src/local/transactions/mutations/commitPayload.ts +11 -1
- package/src/local/transactions/mutations/commitTransport.ts +2 -2
- package/src/local/transactions/mutations/executionSelection.ts +9 -15
- package/src/local/transactions/mutations/failureHandling.ts +10 -0
- package/src/local/transactions/mutations/localMutation.ts +3 -3
- package/src/local/transactions/mutations/queueCoalescing.ts +6 -0
- package/src/presence/index.ts +72 -0
- package/src/react/AbloProvider.tsx +14 -9
- package/src/react/useErrorListener.ts +1 -1
- package/src/react/useMutationFailureListener.ts +1 -1
- package/src/surface.ts +1 -0
- package/dist/local/transactions/mutations/pendingDrain.d.ts +0 -33
- package/dist/local/transactions/mutations/pendingDrain.js +0 -117
- package/dist/presenceStream.d.ts +0 -69
- package/dist/presenceStream.js +0 -200
- package/src/local/transactions/mutations/pendingDrain.ts +0 -169
- 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/
|
|
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
|
|
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
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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:
|
|
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
|
|
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:
|
|
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 {
|
|
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:
|
|
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
|
|
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,
|
|
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++; });
|
package/dist/local/Model.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
410
|
+
type: "create" | "update" | "delete" | "archive" | "unarchive";
|
|
435
411
|
modelName: string;
|
|
436
412
|
modelId: string;
|
|
437
413
|
}[];
|
package/dist/local/SyncClient.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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(
|
|
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.
|
|
1229
|
-
//
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
/**
|