@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
|
@@ -27,8 +27,8 @@ import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
|
27
27
|
export declare const persistedTransactionSchema: z.ZodObject<{
|
|
28
28
|
id: z.ZodString;
|
|
29
29
|
type: z.ZodEnum<{
|
|
30
|
-
update: "update";
|
|
31
30
|
create: "create";
|
|
31
|
+
update: "update";
|
|
32
32
|
delete: "delete";
|
|
33
33
|
archive: "archive";
|
|
34
34
|
unarchive: "unarchive";
|
|
@@ -95,8 +95,8 @@ export declare function deserializePersistedTransaction(row: unknown, runtime?:
|
|
|
95
95
|
export declare const persistedMutationSchema: z.ZodObject<{
|
|
96
96
|
mutationId: z.ZodOptional<z.ZodString>;
|
|
97
97
|
type: z.ZodEnum<{
|
|
98
|
-
update: "update";
|
|
99
98
|
create: "create";
|
|
99
|
+
update: "update";
|
|
100
100
|
delete: "delete";
|
|
101
101
|
archive: "archive";
|
|
102
102
|
}>;
|
|
@@ -138,8 +138,8 @@ export declare const legacyPendingMutationRecordSchema: z.ZodObject<{
|
|
|
138
138
|
type: z.ZodLiteral<"pending_mutation">;
|
|
139
139
|
mutation: z.ZodObject<{
|
|
140
140
|
type: z.ZodEnum<{
|
|
141
|
-
update: "update";
|
|
142
141
|
create: "create";
|
|
142
|
+
update: "update";
|
|
143
143
|
delete: "delete";
|
|
144
144
|
archive: "archive";
|
|
145
145
|
}>;
|
|
@@ -180,8 +180,8 @@ export declare const pendingMutationRecordSchema: z.ZodObject<{
|
|
|
180
180
|
type: z.ZodLiteral<"pending_mutation">;
|
|
181
181
|
mutation: z.ZodObject<{
|
|
182
182
|
type: z.ZodEnum<{
|
|
183
|
-
update: "update";
|
|
184
183
|
create: "create";
|
|
184
|
+
update: "update";
|
|
185
185
|
delete: "delete";
|
|
186
186
|
archive: "archive";
|
|
187
187
|
}>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PresenceProjection, type PresenceProjectionEvents, type PresenceView } from '@abloatai/transaction/presence';
|
|
2
|
+
/** Reactive-client presence backed by the client's existing live connection. */
|
|
3
|
+
export interface ReactivePresence extends PresenceView {
|
|
4
|
+
forModel(model: string, recordId?: string): ReturnType<PresenceProjection['forModel']>;
|
|
5
|
+
onChange(listener: () => void): () => void;
|
|
6
|
+
}
|
|
7
|
+
/** Lifecycle hooks kept inside the humans composition boundary. */
|
|
8
|
+
export interface AttachablePresence extends ReactivePresence {
|
|
9
|
+
attach(transport: PresenceProjectionEvents): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
/** Framework bridge that does not consume a string key on the model namespace. */
|
|
13
|
+
export declare function attachPresenceToClient(client: object, presence: ReactivePresence): void;
|
|
14
|
+
export declare function presenceOfClient(client: object): ReactivePresence;
|
|
15
|
+
export declare function createPresence(transport?: PresenceProjectionEvents | null): AttachablePresence;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createPresenceProjection, } from '@abloatai/transaction/presence';
|
|
2
|
+
const clientPresence = new WeakMap();
|
|
3
|
+
/** Framework bridge that does not consume a string key on the model namespace. */
|
|
4
|
+
export function attachPresenceToClient(client, presence) {
|
|
5
|
+
clientPresence.set(client, presence);
|
|
6
|
+
}
|
|
7
|
+
export function presenceOfClient(client) {
|
|
8
|
+
const presence = clientPresence.get(client);
|
|
9
|
+
if (presence === undefined)
|
|
10
|
+
throw new Error('presence is not attached to this client');
|
|
11
|
+
return presence;
|
|
12
|
+
}
|
|
13
|
+
export function createPresence(transport = null) {
|
|
14
|
+
let projection = null;
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
let unsubscribe = null;
|
|
17
|
+
const notify = () => {
|
|
18
|
+
for (const listener of listeners)
|
|
19
|
+
listener();
|
|
20
|
+
};
|
|
21
|
+
const attach = (events) => {
|
|
22
|
+
if (projection !== null)
|
|
23
|
+
return;
|
|
24
|
+
projection = createPresenceProjection(events);
|
|
25
|
+
unsubscribe = projection.subscribe(notify);
|
|
26
|
+
notify();
|
|
27
|
+
};
|
|
28
|
+
if (transport !== null)
|
|
29
|
+
attach(transport);
|
|
30
|
+
return {
|
|
31
|
+
get active() { return projection?.active ?? []; },
|
|
32
|
+
get others() { return projection?.others ?? []; },
|
|
33
|
+
onChange(listener) {
|
|
34
|
+
listeners.add(listener);
|
|
35
|
+
return () => { listeners.delete(listener); };
|
|
36
|
+
},
|
|
37
|
+
attach,
|
|
38
|
+
forModel(model, recordId) {
|
|
39
|
+
return projection?.forModel(model, recordId) ?? [];
|
|
40
|
+
},
|
|
41
|
+
dispose() {
|
|
42
|
+
unsubscribe?.();
|
|
43
|
+
unsubscribe = null;
|
|
44
|
+
projection?.dispose();
|
|
45
|
+
projection = null;
|
|
46
|
+
listeners.clear();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
3
3
|
import type { AbloClient as Ablo } from '../client.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { PresenceSession } from '@abloatai/transaction/presence';
|
|
5
5
|
import type { GroupScope } from '../local/sync/scopeGroups.js';
|
|
6
6
|
import { type SyncStoreContract } from './context.js';
|
|
7
7
|
/**
|
|
@@ -109,7 +109,7 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
|
109
109
|
export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProviderProps<R>): React.ReactElement;
|
|
110
110
|
export type { GroupScope };
|
|
111
111
|
/**
|
|
112
|
-
* Read-only presence: the
|
|
112
|
+
* Read-only presence: the other sessions currently visible to this
|
|
113
113
|
* connection, bridged to React. This is a pure reader of the engine's
|
|
114
114
|
* already-flowing presence stream; it does not mutate connection groups.
|
|
115
115
|
*
|
|
@@ -127,7 +127,7 @@ export type { GroupScope };
|
|
|
127
127
|
* const alone = !peers.some((p) => p.participantKind === 'user');
|
|
128
128
|
* ```
|
|
129
129
|
*/
|
|
130
|
-
export declare function usePeers(scope?: GroupScope): readonly
|
|
130
|
+
export declare function usePeers(scope?: GroupScope): readonly PresenceSession[];
|
|
131
131
|
/**
|
|
132
132
|
* Returns the raw `SyncEngine` proxy. Typically you want the typed
|
|
133
133
|
* hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
|
|
@@ -7,6 +7,7 @@ import { AbloInternalContext } from './internalContext.js';
|
|
|
7
7
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
8
8
|
import { useSyncStatus } from './useSyncStatus.js';
|
|
9
9
|
import { DefaultFallback } from './DefaultFallback.js';
|
|
10
|
+
import { presenceOfClient } from '../presence/index.js';
|
|
10
11
|
// ── Implementation ───────────────────────────────────────────────────
|
|
11
12
|
/**
|
|
12
13
|
* Lightweight event emitter for provider-level errors. Lives on the
|
|
@@ -201,7 +202,7 @@ function BootstrapGate({ fallback, children, }) {
|
|
|
201
202
|
}
|
|
202
203
|
const EMPTY_PRESENCE = Object.freeze([]);
|
|
203
204
|
/**
|
|
204
|
-
* Read-only presence: the
|
|
205
|
+
* Read-only presence: the other sessions currently visible to this
|
|
205
206
|
* connection, bridged to React. This is a pure reader of the engine's
|
|
206
207
|
* already-flowing presence stream; it does not mutate connection groups.
|
|
207
208
|
*
|
|
@@ -232,10 +233,10 @@ export function usePeers(scope) {
|
|
|
232
233
|
setPeers(EMPTY_PRESENCE);
|
|
233
234
|
return;
|
|
234
235
|
}
|
|
235
|
-
const presence = engine
|
|
236
|
+
const presence = presenceOfClient(engine);
|
|
236
237
|
const compute = () => groups.length === 0
|
|
237
238
|
? presence.others
|
|
238
|
-
: presence.others.filter((
|
|
239
|
+
: presence.others.filter((session) => session.activities.some(({ target }) => target.id !== undefined && groups.includes(`${target.model.toLowerCase()}:${target.id}`)));
|
|
239
240
|
// Plain useState + onChange — presence changes on connect/disconnect/activity
|
|
240
241
|
// only (never on cursor traffic, a separate channel), so this fires
|
|
241
242
|
// rarely; a frame of stale presence is harmless.
|
|
@@ -10,5 +10,5 @@ export function useErrorListener(listener) {
|
|
|
10
10
|
}
|
|
11
11
|
const listenerRef = useRef(listener);
|
|
12
12
|
listenerRef.current = listener;
|
|
13
|
-
useEffect(() => context.subscribeError((error) => listenerRef.current(error)), [context]);
|
|
13
|
+
useEffect(() => context.subscribeError((error) => { listenerRef.current(error); }), [context]);
|
|
14
14
|
}
|
|
@@ -14,6 +14,6 @@ export function useMutationFailureListener(listener) {
|
|
|
14
14
|
const engine = context.engine;
|
|
15
15
|
if (!engine)
|
|
16
16
|
return;
|
|
17
|
-
return engine.onMutationFailure((payload) => listenerRef.current(payload));
|
|
17
|
+
return engine.onMutationFailure((payload) => { listenerRef.current(payload); });
|
|
18
18
|
}, [context, context.engine]);
|
|
19
19
|
}
|
package/dist/surface.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* tuple, so it is the one list of model-verb names a generated summary can
|
|
20
20
|
* describe.
|
|
21
21
|
*/
|
|
22
|
-
export declare const PUBLIC_MODEL_VERBS: readonly ["get", "read", "list", "listAll", "local", "create", "update", "delete", "claim", "onChange"];
|
|
22
|
+
export declare const PUBLIC_MODEL_VERBS: readonly ["get", "read", "list", "listAll", "local", "presence", "create", "update", "delete", "claim", "onChange"];
|
|
23
23
|
/**
|
|
24
24
|
* The option keys accepted by `local.list` and `onChange`, matching the
|
|
25
25
|
* keys of {@link LocalReadOptions}. Note that the lifecycle filter is named
|
package/dist/surface.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"test:property": "jest --testPathPatterns=__tests__/property",
|
|
69
69
|
"test:migrated-integration": "jest --testPathPatterns=__tests__/integration",
|
|
70
70
|
"test:e2e": "E2E_TEST=true jest --config jest.e2e.config.ts",
|
|
71
|
-
"lint:eslint": "eslint . --cache",
|
|
71
|
+
"lint:eslint": "eslint . --cache --suppressions-location eslint-suppressions.json",
|
|
72
72
|
"check:boundary": "node scripts/check-boundary.mjs",
|
|
73
73
|
"test:integration": "npm run build && node scripts/check-package-integration.mjs",
|
|
74
74
|
"lint:pkg": "publint"
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"directory": "packages/humans"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@abloatai/transaction": "^0.
|
|
87
|
+
"@abloatai/transaction": "^0.62.0",
|
|
88
88
|
"mobx": "^6.13.7",
|
|
89
89
|
"uuid": "^11.1.0",
|
|
90
90
|
"zod": "^4.4.3"
|
|
@@ -114,7 +114,6 @@
|
|
|
114
114
|
"react": "^19.2.8",
|
|
115
115
|
"react-dom": "^19.2.8",
|
|
116
116
|
"ts-jest": "^29.4.5",
|
|
117
|
-
"ts-node": "^10.9.2",
|
|
118
117
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
119
118
|
"ws": "^8.18.3"
|
|
120
119
|
}
|
package/src/Ablo.ts
CHANGED
|
@@ -99,7 +99,7 @@ export type Ablo<S extends SchemaRecord> = AbloClient<S>;
|
|
|
99
99
|
* const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
|
|
100
100
|
* ```
|
|
101
101
|
*
|
|
102
|
-
* Server-side agents, workers, and services use `@abloatai/
|
|
102
|
+
* Server-side agents, workers, and services use `@abloatai/ablo`.
|
|
103
103
|
*/
|
|
104
104
|
export function Ablo<
|
|
105
105
|
const S extends SchemaRecord,
|
|
@@ -117,7 +117,15 @@ export function Ablo<const S extends SchemaRecord>(
|
|
|
117
117
|
// resolver, the base URL, the logger, and this participant's identity —
|
|
118
118
|
// and fails on a misconfiguration before anything is constructed.
|
|
119
119
|
const prelude = resolveClientPrelude(options);
|
|
120
|
-
const {
|
|
120
|
+
const {
|
|
121
|
+
internalOptions,
|
|
122
|
+
authCredentials,
|
|
123
|
+
presenceSession,
|
|
124
|
+
logger,
|
|
125
|
+
url,
|
|
126
|
+
participantId,
|
|
127
|
+
kind,
|
|
128
|
+
} = prelude;
|
|
121
129
|
|
|
122
130
|
// 2. The connection, built here in the composition root — before the plugin
|
|
123
131
|
// list resolves, so `PluginContext.transport` carries the instance a
|
|
@@ -134,6 +142,7 @@ export function Ablo<const S extends SchemaRecord>(
|
|
|
134
142
|
baseUrl: url,
|
|
135
143
|
kind,
|
|
136
144
|
getAuthToken: authCredentials.getAuthToken,
|
|
145
|
+
presenceSession,
|
|
137
146
|
collaborationEvents: [...(internalOptions.collaborationEvents ?? [])],
|
|
138
147
|
syncGroups: [...(internalOptions.syncGroups ?? [])],
|
|
139
148
|
deferConnect: true,
|
|
@@ -223,7 +232,7 @@ export function Ablo<const S extends SchemaRecord>(
|
|
|
223
232
|
//
|
|
224
233
|
// One default import, with types hung underneath via namespace dots:
|
|
225
234
|
// `import { Ablo } from "@abloatai/humans"` gets the factory, its return type, and
|
|
226
|
-
// every type a typical consumer references (`Ablo.
|
|
235
|
+
// every type a typical consumer references (`Ablo.PresenceSession`,
|
|
227
236
|
// and so on) — all purely type-level, with zero runtime cost.
|
|
228
237
|
//
|
|
229
238
|
// The types still live in their canonical homes (`types/streams`, `principal`,
|
|
@@ -272,10 +281,10 @@ export namespace Ablo {
|
|
|
272
281
|
export type Duration = _Streams.Duration;
|
|
273
282
|
|
|
274
283
|
// ── Real-time multiplayer (flat — heterogeneous cluster) ──────────
|
|
275
|
-
export type
|
|
284
|
+
export type Presence = import('./presence/index.js').ReactivePresence;
|
|
285
|
+
export type PresenceSession = import('@abloatai/transaction/presence').PresenceSession;
|
|
286
|
+
export type PresenceActivity = import('@abloatai/transaction/presence').PresenceActivity;
|
|
276
287
|
export type ClaimStream = _Streams.ClaimStream;
|
|
277
|
-
export type Peer = _Streams.Peer;
|
|
278
|
-
export type Activity = _Streams.Activity;
|
|
279
288
|
export type Claim = _Streams.Claim;
|
|
280
289
|
export type ClaimRejection = _Streams.ClaimRejection;
|
|
281
290
|
export type ClaimLost = _Streams.ClaimLost;
|
package/src/client.ts
CHANGED
|
@@ -18,7 +18,6 @@ import type {
|
|
|
18
18
|
InferCreate,
|
|
19
19
|
InferRow,
|
|
20
20
|
} from '@abloatai/transaction/schema/schema';
|
|
21
|
-
import type { PresenceStream } from '@abloatai/transaction/types/streams';
|
|
22
21
|
import type { InstanceCache } from './local/InstanceCache.js';
|
|
23
22
|
import type { SyncStoreContract } from './react/context.js';
|
|
24
23
|
import type { SyncWebSocket, CoreSyncEventMap } from './local/sync/SyncWebSocket.js';
|
|
@@ -31,6 +30,7 @@ import type {
|
|
|
31
30
|
import type { EffectiveAuthority } from '@abloatai/transaction/auth';
|
|
32
31
|
import type { ReadDependency } from '@abloatai/transaction/coordination';
|
|
33
32
|
import type { CapturedRow } from '@abloatai/transaction/transport/http';
|
|
33
|
+
import type { ReactivePresence } from './presence/index.js';
|
|
34
34
|
export type { LocalReadOptions } from './local/client/resourceTypes.js';
|
|
35
35
|
|
|
36
36
|
/** The typed sync engine client — one property per model in the schema */
|
|
@@ -232,20 +232,15 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
232
232
|
*/
|
|
233
233
|
readonly syncStatus: SyncStatus;
|
|
234
234
|
|
|
235
|
-
/** The underlying schema */
|
|
236
|
-
readonly schema: Schema<S>;
|
|
237
|
-
|
|
238
235
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* participant for presence does not open a second socket. See
|
|
243
|
-
* {@link PresenceStream}.
|
|
244
|
-
*
|
|
245
|
-
* The reference is stable for the engine's lifetime — the underlying connection
|
|
246
|
-
* is rotated on `dispose()`, but this object stays the same.
|
|
236
|
+
* Session-owned live activity projected from this client's existing
|
|
237
|
+
* connection. Use `active` for every visible activity, `others` to exclude
|
|
238
|
+
* this session, and `forModel(model, id?)` for a model-native view.
|
|
247
239
|
*/
|
|
248
|
-
readonly presence:
|
|
240
|
+
readonly presence: ReactivePresence;
|
|
241
|
+
|
|
242
|
+
/** The underlying schema */
|
|
243
|
+
readonly schema: Schema<S>;
|
|
249
244
|
|
|
250
245
|
/**
|
|
251
246
|
* @internal The supported coordination API is `ablo.<model>.claim`. This
|
package/src/humans.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { AbloPlugin, PluginContext, AppliedChange } from './plugin.js';
|
|
9
9
|
import { AbloValidationError } from '@abloatai/transaction/errors';
|
|
10
|
-
import {
|
|
10
|
+
import { createPresence, type AttachablePresence } from './presence/index.js';
|
|
11
11
|
import {
|
|
12
12
|
buildStoreCluster,
|
|
13
13
|
kStoreCluster,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from './local/client/storeCluster.js';
|
|
17
17
|
|
|
18
18
|
export interface HumansSurface {
|
|
19
|
-
readonly presence:
|
|
19
|
+
readonly presence: AttachablePresence;
|
|
20
20
|
readonly [kStoreCluster]?: StoreCluster;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -45,14 +45,7 @@ export function humans() {
|
|
|
45
45
|
applyChanges = (changes) => { cluster.store.applyChangesToPool(changes); };
|
|
46
46
|
}
|
|
47
47
|
return {
|
|
48
|
-
presence:
|
|
49
|
-
{
|
|
50
|
-
participantId: context.participant?.id ?? '',
|
|
51
|
-
syncGroups: [...(context.syncGroups ?? [])],
|
|
52
|
-
isAgent: context.participant?.kind === 'agent',
|
|
53
|
-
},
|
|
54
|
-
context.transport ?? null,
|
|
55
|
-
),
|
|
48
|
+
presence: createPresence(context.transport ?? null),
|
|
56
49
|
...(cluster ? { [kStoreCluster]: cluster } : {}),
|
|
57
50
|
};
|
|
58
51
|
},
|
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
type GroupRemovedPayload,
|
|
36
36
|
type BootstrapHint,
|
|
37
37
|
type BootstrapDataEvent,
|
|
38
|
-
type PresenceUpdate,
|
|
39
38
|
type EventMap,
|
|
40
39
|
type DefaultCollaborationEvents,
|
|
41
40
|
type SyncWebSocketEventMap,
|
|
@@ -238,7 +237,6 @@ export type {
|
|
|
238
237
|
GroupRemovedPayload,
|
|
239
238
|
BootstrapHint,
|
|
240
239
|
BootstrapDataEvent,
|
|
241
|
-
PresenceUpdate,
|
|
242
240
|
};
|
|
243
241
|
|
|
244
242
|
// deriveSyncPlanFromSchema derives a sync plan from a schema and is
|
|
@@ -1500,7 +1498,6 @@ export class BaseSyncedStore<
|
|
|
1500
1498
|
applyDeltaFrame: (deltas) => { this.applyDeltaFrame(deltas); },
|
|
1501
1499
|
handleBootstrapRequired: (hint) => { this.handleBootstrapRequired(hint); },
|
|
1502
1500
|
handleBootstrapData: (data) => { this.handleBootstrapData(data); },
|
|
1503
|
-
handlePresenceUpdate: (data) => { this.handlePresenceUpdate(data); },
|
|
1504
1501
|
performCredentialRefresh: () => this.performCredentialRefresh(),
|
|
1505
1502
|
handleTerminalSessionError: (error) => { this.terminalSessionLifecycle.start(error); },
|
|
1506
1503
|
nudgeReconnect: () => { this.nudgeReconnect(); },
|
|
@@ -1759,9 +1756,9 @@ export class BaseSyncedStore<
|
|
|
1759
1756
|
const isCreate = !this.objectPool.get(model.id);
|
|
1760
1757
|
if (isCreate) {
|
|
1761
1758
|
model.updatedAt = new Date();
|
|
1762
|
-
this.syncClient.add(model);
|
|
1759
|
+
await this.syncClient.add(model);
|
|
1763
1760
|
} else {
|
|
1764
|
-
this.syncClient.update(model);
|
|
1761
|
+
await this.syncClient.update(model);
|
|
1765
1762
|
}
|
|
1766
1763
|
}
|
|
1767
1764
|
|
|
@@ -1779,21 +1776,21 @@ export class BaseSyncedStore<
|
|
|
1779
1776
|
const model = rowAsModel(entity);
|
|
1780
1777
|
this.pendingDeletes.add(model.id);
|
|
1781
1778
|
// SyncClient.delete handles: pool remove, transaction queue
|
|
1782
|
-
this.syncClient.delete(model);
|
|
1779
|
+
await this.syncClient.delete(model);
|
|
1783
1780
|
}
|
|
1784
1781
|
|
|
1785
1782
|
/** Archive a model. Accepts schema-inferred entity shapes (see `save`). */
|
|
1786
1783
|
async archive<T extends { id: string; archivedAt?: Date | null }>(entity: T): Promise<void> {
|
|
1787
1784
|
const model = rowAsModel(entity);
|
|
1788
1785
|
model.archivedAt = new Date();
|
|
1789
|
-
this.syncClient.archive(model);
|
|
1786
|
+
await this.syncClient.archive(model);
|
|
1790
1787
|
}
|
|
1791
1788
|
|
|
1792
1789
|
/** Unarchive a model. Accepts schema-inferred entity shapes (see `save`). */
|
|
1793
1790
|
async unarchive<T extends { id: string; archivedAt?: Date | null }>(entity: T): Promise<void> {
|
|
1794
1791
|
const model = rowAsModel(entity);
|
|
1795
1792
|
model.archivedAt = null;
|
|
1796
|
-
this.syncClient.update(model);
|
|
1793
|
+
await this.syncClient.update(model);
|
|
1797
1794
|
}
|
|
1798
1795
|
|
|
1799
1796
|
|
|
@@ -1935,9 +1932,6 @@ export class BaseSyncedStore<
|
|
|
1935
1932
|
this.updateSyncStatus({ state: 'syncing' });
|
|
1936
1933
|
}
|
|
1937
1934
|
|
|
1938
|
-
/** Handle presence_update event. Override in subclass. */
|
|
1939
|
-
protected handlePresenceUpdate(_data: PresenceUpdate): void {}
|
|
1940
|
-
|
|
1941
1935
|
// ── Pending changes tracking ─────────────────────────────────────────────
|
|
1942
1936
|
|
|
1943
1937
|
protected incrementPendingChanges(): void {
|
package/src/local/Model.ts
CHANGED
|
@@ -416,7 +416,7 @@ export abstract class Model {
|
|
|
416
416
|
const original = this.getOriginalSnapshot();
|
|
417
417
|
for (const key of keys) {
|
|
418
418
|
if (key === 'id') continue;
|
|
419
|
-
const mod = modified
|
|
419
|
+
const mod = modified.get(key);
|
|
420
420
|
if (mod) {
|
|
421
421
|
out[key] = mod.old;
|
|
422
422
|
} else if (original && key in original) {
|
|
@@ -463,25 +463,23 @@ export abstract class Model {
|
|
|
463
463
|
const modelName = this.getModelName();
|
|
464
464
|
const properties = getActiveRegistry().getProperties(modelName);
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
errors.push(`${propName} is required`);
|
|
474
|
-
}
|
|
466
|
+
const json = this.toJSON();
|
|
467
|
+
for (const [propName, metadata] of properties) {
|
|
468
|
+
// Check required fields
|
|
469
|
+
if (!metadata.nullable && !metadata.optional) {
|
|
470
|
+
const value = json[propName];
|
|
471
|
+
if (value == null || value === '') {
|
|
472
|
+
errors.push(`${propName} is required`);
|
|
475
473
|
}
|
|
474
|
+
}
|
|
476
475
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
476
|
+
// Run custom validation rules
|
|
477
|
+
const rules = this.validationRules[propName];
|
|
478
|
+
if (rules) {
|
|
479
|
+
const value = json[propName];
|
|
480
|
+
for (const rule of rules) {
|
|
481
|
+
const error = rule(value);
|
|
482
|
+
if (error) errors.push(error);
|
|
485
483
|
}
|
|
486
484
|
}
|
|
487
485
|
}
|
|
@@ -816,18 +814,16 @@ export abstract class Model {
|
|
|
816
814
|
result.archivedAt = this.archivedAt?.toISOString() ?? null;
|
|
817
815
|
}
|
|
818
816
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
result[propName] = value;
|
|
830
|
-
}
|
|
817
|
+
const self = this as Record<string, unknown>;
|
|
818
|
+
for (const [propName, metadata] of properties) {
|
|
819
|
+
// Skip certain types
|
|
820
|
+
if (metadata.type === 'ephemeralProperty') continue;
|
|
821
|
+
if (metadata.type === 'referenceModel') continue;
|
|
822
|
+
if (metadata.type === 'referenceCollection') continue;
|
|
823
|
+
|
|
824
|
+
const value = self[propName];
|
|
825
|
+
if (value !== undefined) {
|
|
826
|
+
result[propName] = value;
|
|
831
827
|
}
|
|
832
828
|
}
|
|
833
829
|
|
|
@@ -946,14 +942,12 @@ export abstract class Model {
|
|
|
946
942
|
const modelName = this.getModelName();
|
|
947
943
|
const properties = getActiveRegistry().getProperties(modelName);
|
|
948
944
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
collection.dispose();
|
|
956
|
-
}
|
|
945
|
+
const self = this as Record<string, unknown>;
|
|
946
|
+
for (const [propName, metadata] of properties) {
|
|
947
|
+
if (metadata.type === 'referenceCollection') {
|
|
948
|
+
const collection = self[propName] as Disposable | undefined;
|
|
949
|
+
if (collection) {
|
|
950
|
+
collection.dispose();
|
|
957
951
|
}
|
|
958
952
|
}
|
|
959
953
|
}
|
|
@@ -983,11 +977,9 @@ export abstract class Model {
|
|
|
983
977
|
const modelName = this.getModelName();
|
|
984
978
|
const properties = getActiveRegistry().getProperties(modelName);
|
|
985
979
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
snapshot[propName] = json[propName];
|
|
990
|
-
}
|
|
980
|
+
const json = this.toJSON();
|
|
981
|
+
for (const [propName] of properties) {
|
|
982
|
+
snapshot[propName] = json[propName];
|
|
991
983
|
}
|
|
992
984
|
|
|
993
985
|
return snapshot;
|
|
@@ -1034,19 +1026,17 @@ export abstract class Model {
|
|
|
1034
1026
|
|
|
1035
1027
|
const properties = getActiveRegistry().getProperties(this.getModelName());
|
|
1036
1028
|
const self = this as Record<string, unknown>;
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
// Reading through the observable getter is the point: it subscribes the
|
|
1047
|
-
// enclosing MobX reaction to this field.
|
|
1048
|
-
snapshot[propName] = self[propName];
|
|
1029
|
+
for (const [propName, metadata] of properties) {
|
|
1030
|
+
if (
|
|
1031
|
+
metadata.type === 'ephemeralProperty' ||
|
|
1032
|
+
metadata.type === 'referenceModel' ||
|
|
1033
|
+
metadata.type === 'referenceCollection'
|
|
1034
|
+
) {
|
|
1035
|
+
continue;
|
|
1049
1036
|
}
|
|
1037
|
+
// Reading through the observable getter is the point: it subscribes the
|
|
1038
|
+
// enclosing MobX reaction to this field.
|
|
1039
|
+
snapshot[propName] = self[propName];
|
|
1050
1040
|
}
|
|
1051
1041
|
|
|
1052
1042
|
for (const name of this.getDerivedGetterNames()) {
|
|
@@ -14,6 +14,8 @@ export class NetworkMonitor extends EventEmitter {
|
|
|
14
14
|
// Only `navigator.onLine === false` means offline. Node 18+ exposes a global
|
|
15
15
|
// `navigator` with `onLine === undefined`, so the naive `navigator.onLine`
|
|
16
16
|
// would seed `false` (offline) on every server client — start optimistic.
|
|
17
|
+
// DOM types say `onLine` is boolean, but Node exposes it as undefined.
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
|
17
19
|
private isOnline = !(typeof navigator !== 'undefined' && navigator.onLine === false);
|
|
18
20
|
private lastOnlineCheck: Date = new Date();
|
|
19
21
|
|
|
@@ -106,6 +106,8 @@ export const browserOnlineStatus: OnlineStatusProvider = {
|
|
|
106
106
|
// signal. Don't use `!navigator.onLine`: Node 18+ exposes a global
|
|
107
107
|
// `navigator` whose `onLine` is `undefined`, which `!` would read as offline —
|
|
108
108
|
// wedging every Node/server client (agents, worker, MCP) into a false offline.
|
|
109
|
+
// DOM types say `onLine` is boolean, but Node exposes it as undefined.
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
|
109
111
|
return !(typeof navigator !== 'undefined' && navigator.onLine === false);
|
|
110
112
|
},
|
|
111
113
|
};
|