@abloatai/ablo 0.3.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 (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. package/package.json +147 -0
@@ -0,0 +1,114 @@
1
+ import type { SyncWebSocket } from './SyncWebSocket.js';
2
+ import type { Schema, SchemaRecord } from '../schema/schema.js';
3
+ import type { ActiveIntent, Activity, EntityRef, Claim, IntentStream, Peer, PresenceStream, PresenceTarget } from '../types/streams.js';
4
+ /**
5
+ * Scope accepted by participant APIs. The normal SDK shape is an
6
+ * entity target (`{ type, id }`). Raw sync-group strings remain an
7
+ * advanced transport escape hatch.
8
+ */
9
+ export type ParticipantScope = EntityRef | readonly EntityRef[] | string | readonly string[] | {
10
+ readonly syncGroup: string;
11
+ } | {
12
+ readonly syncGroups: readonly string[];
13
+ } | Record<string, string | readonly string[] | undefined>;
14
+ export type ParticipantStatus = 'idle' | 'connecting' | 'connected' | 'error' | 'disconnected';
15
+ export interface EngineParticipant {
16
+ readonly presence: PresenceStream;
17
+ readonly intents: IntentStream;
18
+ }
19
+ export interface ParticipantJoinOptions {
20
+ /**
21
+ * Initial focus target: customer schema vocabulary, optionally
22
+ * narrowed to a path, field, or range. When `scope` is omitted,
23
+ * this also becomes the routing scope.
24
+ */
25
+ readonly target?: PresenceTarget;
26
+ /** Alias for `target` when the participant is joined to a broader scope. */
27
+ readonly focus?: PresenceTarget;
28
+ /**
29
+ * Routing scope. Can be one entity, many entities, or a raw
30
+ * sync-group escape hatch. Use this for "joined to folder, focused
31
+ * on file" shapes.
32
+ */
33
+ readonly scope?: ParticipantScope;
34
+ /** Present a narrower capability for this logical participant. */
35
+ readonly capabilityToken?: string;
36
+ /** Claim TTL, in seconds or a compact duration string (`30s`, `5m`). */
37
+ readonly ttlSeconds?: number | string | null;
38
+ /**
39
+ * Activity to announce immediately after the claim acks. Defaults to
40
+ * `reading` when `target` is present. Pass false to join silently.
41
+ */
42
+ readonly activity?: 'reading' | 'viewing' | 'editing' | false;
43
+ readonly detail?: string;
44
+ }
45
+ export interface ScopedPresence {
46
+ readonly self: Peer;
47
+ readonly focus: EntityRef | null;
48
+ readonly others: ReadonlyArray<Peer>;
49
+ update(activity: Activity): void;
50
+ reading(detail?: string): void;
51
+ reading(target: PresenceTarget, detail?: string): void;
52
+ viewing(detail?: string): void;
53
+ viewing(target: PresenceTarget, detail?: string): void;
54
+ editing(detail?: string): void;
55
+ editing(target: PresenceTarget, detail?: string): void;
56
+ idle(): void;
57
+ subscribe(listener: () => void): () => void;
58
+ }
59
+ export interface ScopedClaimOptions {
60
+ /** Override the participant's focus target for this one claim. */
61
+ readonly target?: PresenceTarget;
62
+ /** Free-form reason. Defaults to `'editing'`. Common: `'editing'`,
63
+ * `'writing'`, `'reviewing'`, app-specific phases. */
64
+ readonly reason?: string;
65
+ /** TTL — server auto-expires the claim after this. */
66
+ readonly ttl?: import('../types/streams.js').Duration;
67
+ }
68
+ export interface ScopedIntents {
69
+ readonly focus: EntityRef | null;
70
+ readonly others: ReadonlyArray<ActiveIntent>;
71
+ /**
72
+ * Claim an exclusive intent on the participant's focus target (or
73
+ * an explicit override via `opts.target`). Single verb — the old
74
+ * `editing / writing / announce / claim(reason, opts)` overloads
75
+ * collapsed into this one method.
76
+ */
77
+ claim(opts?: ScopedClaimOptions): Claim;
78
+ onRejected(listener: Parameters<IntentStream['onRejected']>[0]): () => void;
79
+ subscribe(listener: () => void): () => void;
80
+ }
81
+ export interface ParticipantFocusOptions {
82
+ readonly activity?: 'reading' | 'viewing' | 'editing' | false;
83
+ readonly detail?: string;
84
+ }
85
+ export interface JoinedParticipant {
86
+ /** Current exact thing this participant is reading/editing. */
87
+ readonly target: EntityRef | null;
88
+ readonly focusTarget: EntityRef | null;
89
+ /** Transport scopes this participant is joined to for visibility/fan-out. */
90
+ readonly syncGroups: readonly string[];
91
+ readonly presence: ScopedPresence;
92
+ readonly intents: ScopedIntents;
93
+ readonly peers: ReadonlyArray<Peer>;
94
+ readonly claims: ReadonlyArray<ActiveIntent>;
95
+ focus(target: PresenceTarget, options?: ParticipantFocusOptions): JoinedParticipant;
96
+ leave(): void;
97
+ [Symbol.asyncDispose](): Promise<void>;
98
+ }
99
+ export interface ParticipantManager {
100
+ join(target: PresenceTarget, options?: Omit<ParticipantJoinOptions, 'target'>): Promise<JoinedParticipant>;
101
+ join(options: ParticipantJoinOptions): Promise<JoinedParticipant>;
102
+ }
103
+ export interface ParticipantManagerConfig {
104
+ readonly ready: () => Promise<void>;
105
+ readonly getTransport: () => SyncWebSocket | null;
106
+ readonly presence: PresenceStream;
107
+ readonly intents: IntentStream;
108
+ readonly schema?: Schema<SchemaRecord>;
109
+ }
110
+ export declare function createParticipantManager(config: ParticipantManagerConfig): ParticipantManager;
111
+ export declare function resolveParticipantSyncGroups(scope: ParticipantScope | undefined, schema?: Schema<SchemaRecord>): string[];
112
+ export declare function syncGroupFromEntityRef(ref: EntityRef, schema?: Schema<SchemaRecord>): string;
113
+ export declare function parseParticipantTtlSeconds(value: number | string | null | undefined): number | undefined;
114
+ export declare function createParticipantClaimId(): string;
@@ -0,0 +1,336 @@
1
+ export function createParticipantManager(config) {
2
+ return {
3
+ async join(input, overrides) {
4
+ const options = normalizeJoinOptions(input, overrides);
5
+ const target = options.focus ?? options.target
6
+ ? targetToEntityRef((options.focus ?? options.target))
7
+ : null;
8
+ const syncGroups = unique(resolveParticipantSyncGroups(options.scope ?? target ?? undefined, config.schema));
9
+ await config.ready();
10
+ const transport = config.getTransport();
11
+ if (!transport) {
12
+ throw new Error('Ablo participant join failed: WebSocket is not connected');
13
+ }
14
+ const claimId = createParticipantClaimId();
15
+ if (syncGroups.length > 0) {
16
+ await transport.sendClaim(claimId, syncGroups, {
17
+ capabilityToken: options.capabilityToken,
18
+ ttlSeconds: parseParticipantTtlSeconds(options.ttlSeconds),
19
+ });
20
+ }
21
+ const participant = createJoinedParticipant({
22
+ target,
23
+ syncGroups,
24
+ claimId,
25
+ transport,
26
+ presence: config.presence,
27
+ intents: config.intents,
28
+ });
29
+ if (target && options.activity !== false) {
30
+ const activity = options.activity ?? 'reading';
31
+ if (activity === 'editing') {
32
+ participant.presence.editing(options.detail);
33
+ }
34
+ else if (activity === 'viewing') {
35
+ participant.presence.viewing(options.detail);
36
+ }
37
+ else {
38
+ participant.presence.reading(options.detail);
39
+ }
40
+ }
41
+ return participant;
42
+ },
43
+ };
44
+ }
45
+ export function resolveParticipantSyncGroups(scope, schema) {
46
+ if (!scope)
47
+ return [];
48
+ if (typeof scope === 'string')
49
+ return [scope];
50
+ if (Array.isArray(scope)) {
51
+ return scope.flatMap((entry) => typeof entry === 'string' ? [entry] : [syncGroupFromEntityRef(entry, schema)]);
52
+ }
53
+ const direct = scope;
54
+ if (isEntityScope(scope))
55
+ return [syncGroupFromEntityRef(scope, schema)];
56
+ if (typeof direct.syncGroup === 'string')
57
+ return [direct.syncGroup];
58
+ if (Array.isArray(direct.syncGroups)) {
59
+ return direct.syncGroups.filter((g) => typeof g === 'string');
60
+ }
61
+ const out = [];
62
+ for (const [key, value] of Object.entries(scope)) {
63
+ if (value === undefined)
64
+ continue;
65
+ if (Array.isArray(value)) {
66
+ for (const id of value)
67
+ out.push(syncGroupFromSchemaKey(key, id, schema));
68
+ }
69
+ else {
70
+ out.push(syncGroupFromSchemaKey(key, value, schema));
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ export function syncGroupFromEntityRef(ref, schema) {
76
+ const match = findModelForEntityRef(ref, schema);
77
+ if (match?.def.syncGroupFormat) {
78
+ return renderSyncGroupFormat(match.def.syncGroupFormat, { id: ref.id });
79
+ }
80
+ return `${ref.type.toLowerCase()}:${ref.id}`;
81
+ }
82
+ function syncGroupFromSchemaKey(schemaKey, id, schema) {
83
+ const def = schema?.models?.[schemaKey];
84
+ if (def?.syncGroupFormat) {
85
+ return renderSyncGroupFormat(def.syncGroupFormat, { id });
86
+ }
87
+ return `${schemaKey}:${id}`;
88
+ }
89
+ function findModelForEntityRef(ref, schema) {
90
+ if (!schema?.models)
91
+ return null;
92
+ const wanted = ref.type.toLowerCase();
93
+ for (const [key, def] of Object.entries(schema.models)) {
94
+ const typename = def.typename ?? key;
95
+ if (typename.toLowerCase() === wanted || key.toLowerCase() === wanted) {
96
+ return { key, def };
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ function renderSyncGroupFormat(format, values) {
102
+ return format.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_match, key) => {
103
+ const value = values[key];
104
+ return value === undefined ? `{${key}}` : value;
105
+ });
106
+ }
107
+ export function parseParticipantTtlSeconds(value) {
108
+ if (typeof value === 'number' && Number.isFinite(value))
109
+ return value;
110
+ if (!value)
111
+ return undefined;
112
+ if (typeof value !== 'string')
113
+ return undefined;
114
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(value.trim());
115
+ if (!match)
116
+ return undefined;
117
+ const amount = Number(match[1]);
118
+ const unit = match[2] ?? 's';
119
+ if (unit === 'ms')
120
+ return Math.max(1, Math.ceil(amount / 1000));
121
+ if (unit === 'm')
122
+ return Math.ceil(amount * 60);
123
+ if (unit === 'h')
124
+ return Math.ceil(amount * 3600);
125
+ return Math.ceil(amount);
126
+ }
127
+ export function createParticipantClaimId() {
128
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
129
+ return `participant:${crypto.randomUUID()}`;
130
+ }
131
+ return `participant:${Date.now()}:${Math.random().toString(36).slice(2)}`;
132
+ }
133
+ function normalizeJoinOptions(input, overrides) {
134
+ if (isTupleTarget(input) || isEntityScope(input)) {
135
+ return { ...overrides, target: input };
136
+ }
137
+ return { ...input, ...overrides };
138
+ }
139
+ function isTupleTarget(value) {
140
+ return (Array.isArray(value) &&
141
+ typeof value[0] === 'string' &&
142
+ typeof value[1] === 'string');
143
+ }
144
+ function isEntityScope(scope) {
145
+ return (typeof scope === 'object' &&
146
+ scope !== null &&
147
+ !Array.isArray(scope) &&
148
+ typeof scope.type === 'string' &&
149
+ typeof scope.id === 'string');
150
+ }
151
+ function targetToEntityRef(target) {
152
+ if (isTupleTarget(target))
153
+ return { type: target[0], id: target[1] };
154
+ return target;
155
+ }
156
+ function unique(values) {
157
+ return [...new Set(values)];
158
+ }
159
+ function createJoinedParticipant(args) {
160
+ const ownHandles = new Set();
161
+ let currentTarget = args.target;
162
+ let left = false;
163
+ const requireTarget = (target) => {
164
+ const resolved = target ? targetToEntityRef(target) : currentTarget;
165
+ if (!resolved) {
166
+ throw new Error('Participant action requires a structured target');
167
+ }
168
+ return resolved;
169
+ };
170
+ const setFocus = (target, options) => {
171
+ currentTarget = targetToEntityRef(target);
172
+ if (options?.activity === 'editing') {
173
+ scopedPresence.editing(options.detail);
174
+ }
175
+ else if (options?.activity === 'viewing') {
176
+ scopedPresence.viewing(options.detail);
177
+ }
178
+ else if (options?.activity === 'reading') {
179
+ scopedPresence.reading(options.detail);
180
+ }
181
+ return joined;
182
+ };
183
+ const resolvePresenceAction = (targetOrDetail, detail) => {
184
+ if (typeof targetOrDetail === 'string' || targetOrDetail === undefined) {
185
+ return { target: requireTarget(), detail: targetOrDetail ?? detail };
186
+ }
187
+ return { target: requireTarget(targetOrDetail), detail };
188
+ };
189
+ const scopedPresence = {
190
+ get self() {
191
+ return args.presence.self;
192
+ },
193
+ get focus() {
194
+ return currentTarget;
195
+ },
196
+ get others() {
197
+ return args.presence.others.filter((entry) => presenceMatchesParticipant(entry, currentTarget, args.syncGroups));
198
+ },
199
+ update(activity) {
200
+ args.presence.update(activity);
201
+ },
202
+ reading(targetOrDetail, detail) {
203
+ const action = resolvePresenceAction(targetOrDetail, detail);
204
+ args.presence.update({
205
+ ...activityFromTarget(action.target),
206
+ action: 'reading',
207
+ detail: action.detail,
208
+ });
209
+ },
210
+ viewing(targetOrDetail, detail) {
211
+ const action = resolvePresenceAction(targetOrDetail, detail);
212
+ args.presence.viewing(action.target, action.detail);
213
+ },
214
+ editing(targetOrDetail, detail) {
215
+ const action = resolvePresenceAction(targetOrDetail, detail);
216
+ args.presence.editing(action.target, action.detail);
217
+ },
218
+ idle() {
219
+ args.presence.idle();
220
+ },
221
+ subscribe(listener) {
222
+ return args.presence.subscribe(listener);
223
+ },
224
+ };
225
+ const track = (handle) => {
226
+ ownHandles.add(handle);
227
+ return {
228
+ id: handle.id,
229
+ revoke() {
230
+ ownHandles.delete(handle);
231
+ handle.revoke();
232
+ },
233
+ [Symbol.asyncDispose]: async () => {
234
+ ownHandles.delete(handle);
235
+ await handle[Symbol.asyncDispose]();
236
+ },
237
+ };
238
+ };
239
+ const scopedIntents = {
240
+ get focus() {
241
+ return currentTarget;
242
+ },
243
+ get others() {
244
+ return args.intents.others.filter((intent) => currentTarget ? targetsOverlap(intent.target, currentTarget) : true);
245
+ },
246
+ claim(opts) {
247
+ return track(args.intents.claim(requireTarget(opts?.target), {
248
+ reason: opts?.reason,
249
+ ttl: opts?.ttl,
250
+ }));
251
+ },
252
+ onRejected(listener) {
253
+ return args.intents.onRejected(listener);
254
+ },
255
+ subscribe(listener) {
256
+ return args.intents.subscribe(listener);
257
+ },
258
+ };
259
+ const leave = () => {
260
+ if (left)
261
+ return;
262
+ left = true;
263
+ for (const handle of Array.from(ownHandles)) {
264
+ handle.revoke();
265
+ ownHandles.delete(handle);
266
+ }
267
+ args.presence.idle();
268
+ if (args.syncGroups.length > 0) {
269
+ args.transport.sendRelease(args.claimId);
270
+ }
271
+ };
272
+ const joined = {
273
+ get target() {
274
+ return currentTarget;
275
+ },
276
+ get focusTarget() {
277
+ return currentTarget;
278
+ },
279
+ syncGroups: [...args.syncGroups],
280
+ presence: scopedPresence,
281
+ intents: scopedIntents,
282
+ get peers() {
283
+ return scopedPresence.others;
284
+ },
285
+ get claims() {
286
+ return scopedIntents.others;
287
+ },
288
+ focus: setFocus,
289
+ leave,
290
+ [Symbol.asyncDispose]: async () => {
291
+ leave();
292
+ },
293
+ };
294
+ return joined;
295
+ }
296
+ function activityFromTarget(target) {
297
+ return {
298
+ entityType: target.type,
299
+ entityId: target.id,
300
+ path: target.path,
301
+ range: target.range,
302
+ field: target.field,
303
+ meta: target.meta,
304
+ };
305
+ }
306
+ function presenceMatchesParticipant(entry, target, syncGroups) {
307
+ if (syncGroups.some((g) => entry.syncGroups.includes(g)))
308
+ return true;
309
+ if (!target)
310
+ return true;
311
+ return targetsOverlap({
312
+ type: entry.activity.entityType,
313
+ id: entry.activity.entityId,
314
+ path: entry.activity.path,
315
+ range: entry.activity.range,
316
+ field: entry.activity.field,
317
+ meta: entry.activity.meta,
318
+ }, target);
319
+ }
320
+ function targetsOverlap(a, b) {
321
+ if (a.type !== b.type || a.id !== b.id)
322
+ return false;
323
+ if (!hasSubtarget(a) || !hasSubtarget(b))
324
+ return true;
325
+ if (a.path && b.path && a.path !== b.path)
326
+ return false;
327
+ const fieldOverlaps = !a.field || !b.field || a.field === b.field;
328
+ const rangeOverlaps = !a.range || !b.range || rangesOverlap(a.range, b.range);
329
+ return fieldOverlaps && rangeOverlaps;
330
+ }
331
+ function hasSubtarget(target) {
332
+ return Boolean(target.path || target.field || target.range);
333
+ }
334
+ function rangesOverlap(a, b) {
335
+ return a.startLine <= b.endLine && b.startLine <= a.endLine;
336
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Zod validation schemas for sync engine server responses.
3
+ *
4
+ * Validates data at the fetch boundary before it enters the sync engine.
5
+ * Uses .passthrough() so the server can add fields without breaking clients.
6
+ */
7
+ import { z } from 'zod';
8
+ export declare const ServerDeltaSchema: z.ZodObject<{
9
+ id: z.ZodNumber;
10
+ operation: z.ZodOptional<z.ZodEnum<{
11
+ A: "A";
12
+ I: "I";
13
+ U: "U";
14
+ D: "D";
15
+ C: "C";
16
+ G: "G";
17
+ S: "S";
18
+ V: "V";
19
+ }>>;
20
+ action: z.ZodOptional<z.ZodEnum<{
21
+ A: "A";
22
+ I: "I";
23
+ U: "U";
24
+ D: "D";
25
+ C: "C";
26
+ G: "G";
27
+ S: "S";
28
+ V: "V";
29
+ }>>;
30
+ modelName: z.ZodString;
31
+ entityId: z.ZodOptional<z.ZodString>;
32
+ modelId: z.ZodOptional<z.ZodString>;
33
+ data: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
34
+ }, z.core.$loose>;
35
+ export type ValidatedServerDelta = z.infer<typeof ServerDeltaSchema>;
36
+ export declare const BootstrapResponseSchema: z.ZodObject<{
37
+ type: z.ZodEnum<{
38
+ partial: "partial";
39
+ full: "full";
40
+ }>;
41
+ lastSyncId: z.ZodNumber;
42
+ models: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodPipe<z.ZodUnion<readonly [z.ZodArray<z.ZodUnknown>, z.ZodString, z.ZodNull]>, z.ZodTransform<unknown[], string | unknown[] | null>>>>;
43
+ deltas: z.ZodOptional<z.ZodArray<z.ZodObject<{
44
+ id: z.ZodNumber;
45
+ operation: z.ZodOptional<z.ZodEnum<{
46
+ A: "A";
47
+ I: "I";
48
+ U: "U";
49
+ D: "D";
50
+ C: "C";
51
+ G: "G";
52
+ S: "S";
53
+ V: "V";
54
+ }>>;
55
+ action: z.ZodOptional<z.ZodEnum<{
56
+ A: "A";
57
+ I: "I";
58
+ U: "U";
59
+ D: "D";
60
+ C: "C";
61
+ G: "G";
62
+ S: "S";
63
+ V: "V";
64
+ }>>;
65
+ modelName: z.ZodString;
66
+ entityId: z.ZodOptional<z.ZodString>;
67
+ modelId: z.ZodOptional<z.ZodString>;
68
+ data: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
69
+ }, z.core.$loose>>>;
70
+ deltaCount: z.ZodOptional<z.ZodNumber>;
71
+ failedModels: z.ZodOptional<z.ZodArray<z.ZodString>>;
72
+ timestamp: z.ZodDefault<z.ZodNumber>;
73
+ }, z.core.$loose>;
74
+ export type ValidatedBootstrapResponse = z.infer<typeof BootstrapResponseSchema>;
75
+ /**
76
+ * Validate a raw bootstrap response from the server.
77
+ * Logs validation failures via SyncObservability and throws a descriptive error.
78
+ */
79
+ export declare function parseBootstrapResponse(raw: unknown): ValidatedBootstrapResponse;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Zod validation schemas for sync engine server responses.
3
+ *
4
+ * Validates data at the fetch boundary before it enters the sync engine.
5
+ * Uses .passthrough() so the server can add fields without breaking clients.
6
+ */
7
+ import { z } from 'zod';
8
+ import { getContext } from "../context.js";
9
+ import { AbloValidationError } from "../errors.js";
10
+ // ─── Sync Action Types ───────────────────────────────────────────────────────
11
+ // Mirror of SyncActionType from sync-engine/types.ts
12
+ const SYNC_ACTION_VALUES = ['I', 'U', 'D', 'A', 'C', 'G', 'S', 'V'];
13
+ // ─── Server Delta Schema ─────────────────────────────────────────────────────
14
+ export const ServerDeltaSchema = z
15
+ .object({
16
+ id: z.number(),
17
+ operation: z.enum(SYNC_ACTION_VALUES).optional(),
18
+ action: z.enum(SYNC_ACTION_VALUES).optional(),
19
+ modelName: z.string(),
20
+ entityId: z.string().optional(),
21
+ modelId: z.string().optional(),
22
+ data: z.record(z.string(), z.unknown()).nullable().optional(),
23
+ })
24
+ .passthrough();
25
+ // ─── Model Value Schema ─────────────────────────────────────────────────────
26
+ // Server model values arrive in multiple shapes depending on Go serialization:
27
+ // - Array: already-parsed JSON array (most common)
28
+ // - String: double-encoded JSON string from json.RawMessage
29
+ // - null: from PostgreSQL jsonb_agg with no matching rows
30
+ // This schema normalizes all variants into unknown[] before downstream use.
31
+ const ModelValueSchema = z
32
+ .union([z.array(z.unknown()), z.string(), z.null()])
33
+ .transform((val) => {
34
+ if (val === null)
35
+ return [];
36
+ if (typeof val === 'string') {
37
+ try {
38
+ const parsed = JSON.parse(val);
39
+ return Array.isArray(parsed) ? parsed : [];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ return val;
46
+ });
47
+ // ─── Bootstrap Response Schema ───────────────────────────────────────────────
48
+ export const BootstrapResponseSchema = z
49
+ .object({
50
+ type: z.enum(['full', 'partial']),
51
+ lastSyncId: z.number(),
52
+ models: z.record(z.string(), ModelValueSchema).optional(),
53
+ deltas: z.array(ServerDeltaSchema).optional(),
54
+ deltaCount: z.number().optional(),
55
+ failedModels: z.array(z.string()).optional(),
56
+ timestamp: z.number().default(() => Date.now()),
57
+ })
58
+ .passthrough();
59
+ // ─── Parse Helpers ───────────────────────────────────────────────────────────
60
+ /**
61
+ * Validate a raw bootstrap response from the server.
62
+ * Logs validation failures via SyncObservability and throws a descriptive error.
63
+ */
64
+ export function parseBootstrapResponse(raw) {
65
+ const result = BootstrapResponseSchema.safeParse(raw);
66
+ if (!result.success) {
67
+ const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
68
+ getContext().observability.breadcrumb('Bootstrap response validation failed', 'sync.bootstrap', 'error', {
69
+ issues,
70
+ rawType: typeof raw,
71
+ rawKeys: raw && typeof raw === 'object' ? Object.keys(raw).join(',') : 'n/a',
72
+ });
73
+ throw new AbloValidationError(`Invalid bootstrap response: ${issues}`, {
74
+ code: 'bootstrap_response_schema_invalid',
75
+ });
76
+ }
77
+ return result.data;
78
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Bootstrap response factories for sync engine tests.
3
+ *
4
+ * Creates well-formed bootstrap responses matching the server API.
5
+ */
6
+ import type { BootstrapType } from '../../types/index.js';
7
+ export interface BootstrapModelData {
8
+ [modelName: string]: Array<Record<string, unknown>>;
9
+ }
10
+ export interface BootstrapResponse {
11
+ type: BootstrapType;
12
+ lastSyncId: number;
13
+ models?: BootstrapModelData;
14
+ deltas?: Array<{
15
+ id: number;
16
+ modelName: string;
17
+ modelId: string;
18
+ action: string;
19
+ data: Record<string, unknown>;
20
+ }>;
21
+ deltaCount?: number;
22
+ failedModels?: string[];
23
+ timestamp: number;
24
+ }
25
+ /**
26
+ * Create a full bootstrap response (fresh snapshot from server).
27
+ */
28
+ export declare function createFullBootstrapResponse(models: BootstrapModelData, lastSyncId?: number): BootstrapResponse;
29
+ /**
30
+ * Create a partial bootstrap response (delta batch from lastSyncId).
31
+ */
32
+ export declare function createPartialBootstrapResponse(deltas: BootstrapResponse['deltas'], lastSyncId: number): BootstrapResponse;
33
+ /**
34
+ * Create a full bootstrap response with test model data pre-populated.
35
+ */
36
+ export declare function createTestBootstrapResponse(options?: {
37
+ tasks?: Array<Record<string, unknown>>;
38
+ projects?: Array<Record<string, unknown>>;
39
+ slideDecks?: Array<Record<string, unknown>>;
40
+ slides?: Array<Record<string, unknown>>;
41
+ slideLayers?: Array<Record<string, unknown>>;
42
+ comments?: Array<Record<string, unknown>>;
43
+ lastSyncId?: number;
44
+ failedModels?: string[];
45
+ }): BootstrapResponse;