@abloatai/humans 0.51.0 → 0.52.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 (37) hide show
  1. package/dist/client.d.ts +4 -4
  2. package/dist/local/Database.d.ts +1 -34
  3. package/dist/local/Database.js +1 -57
  4. package/dist/local/Model.js +0 -18
  5. package/dist/local/SyncClient.d.ts +0 -2
  6. package/dist/local/SyncClient.js +2 -19
  7. package/dist/local/client/createModelProxy.js +1 -1
  8. package/dist/local/interfaces/index.d.ts +3 -3
  9. package/dist/local/sync/OnDemandLoader.js +1 -1
  10. package/dist/local/sync/deltaPipeline.js +1 -1
  11. package/dist/local/sync/initialize.js +2 -2
  12. package/dist/local/transactions/mutations/MutationQueue.d.ts +1 -1
  13. package/dist/local/transactions/mutations/MutationQueue.js +1 -1
  14. package/dist/local/transactions/persistedTransaction.d.ts +39 -0
  15. package/dist/local/transactions/persistedTransaction.js +53 -0
  16. package/dist/local/utils/mobxSetup.js +1 -1
  17. package/dist/react/AbloProvider.d.ts +2 -2
  18. package/dist/react/AbloProvider.js +2 -2
  19. package/dist/react/context.d.ts +2 -2
  20. package/dist/react/useAblo.d.ts +3 -3
  21. package/package.json +2 -2
  22. package/src/client.ts +4 -4
  23. package/src/local/BaseSyncedStore.ts +1 -1
  24. package/src/local/Database.ts +5 -113
  25. package/src/local/Model.ts +0 -20
  26. package/src/local/SyncClient.ts +2 -28
  27. package/src/local/client/createModelProxy.ts +2 -2
  28. package/src/local/interfaces/index.ts +3 -3
  29. package/src/local/sync/OnDemandLoader.ts +1 -1
  30. package/src/local/sync/deltaPipeline.ts +1 -1
  31. package/src/local/sync/initialize.ts +2 -2
  32. package/src/local/transactions/mutations/MutationQueue.ts +1 -1
  33. package/src/local/transactions/persistedTransaction.ts +112 -0
  34. package/src/local/utils/mobxSetup.ts +1 -1
  35. package/src/react/AbloProvider.tsx +2 -2
  36. package/src/react/context.ts +2 -2
  37. package/src/react/useAblo.ts +3 -3
package/dist/client.d.ts CHANGED
@@ -108,10 +108,10 @@ export type AbloClient<S extends SchemaRecord> = {
108
108
  * server verifies it. The browser must never see the `sk_` key, only the
109
109
  * per-user session token.
110
110
  *
111
- * Pass `{ user: { id }, can: { tasks: ['read', 'update'] } }` for an end-user
111
+ * Pass `{ user: { id }, can: { items: ['read', 'update'] } }` for an end-user
112
112
  * session. It mints an `ek_` and attributes writes to a user (recorded as
113
113
  * `actor_kind` on the delta row). Pass `{ agent: { id }, can: {
114
- * tasks: ['update'] } }` for a scoped agent session, which mints an `rk_`.
114
+ * items: ['update'] } }` for a scoped agent session, which mints an `rk_`.
115
115
  * Both kinds require `can`, typed against your schema's model names. This
116
116
  * always authenticates with the original `sk_`, never the client's exchanged
117
117
  * sync credential.
@@ -126,10 +126,10 @@ export type AbloClient<S extends SchemaRecord> = {
126
126
  * ```ts
127
127
  * const agent = await ablo.agents.create({
128
128
  * name: 'researcher', // readable label (optional)
129
- * can: { documents: ['read', 'update'] },
129
+ * can: { records: ['read', 'update'] },
130
130
  * // id omitted → a fresh uuid: a distinct, independent participant
131
131
  * });
132
- * await agent.documents.update({ id, data, claim });
132
+ * await agent.records.update({ id, data, claim });
133
133
  * await agent.dispose(); // when the agent is done
134
134
  * ```
135
135
  *
@@ -14,43 +14,10 @@ import type { AppliedChange } from '../plugin.js';
14
14
  import type { BootstrapFetcher, BootstrapData } from './sync/BootstrapFetcher.js';
15
15
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
16
16
  import type { SyncDeltaAction } from '@abloatai/transaction/wire/delta';
17
- import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
18
17
  import type { BootstrapType } from '@abloatai/transaction/types';
18
+ import { type PersistedTransaction } from './transactions/persistedTransaction.js';
19
19
  /** Generic record type for model data */
20
20
  type ModelData = Record<string, unknown>;
21
- /** Persisted mutation in a transaction */
22
- interface PersistedMutation {
23
- type: 'create' | 'update' | 'delete' | 'archive';
24
- modelData: ModelData;
25
- modelName: string;
26
- timestamp: string;
27
- writeOptions?: {
28
- readAt?: number | null;
29
- onStale?: OnStaleMode | null;
30
- };
31
- }
32
- /** Persisted transaction for offline/retry support.
33
- *
34
- * Index signature is part of the contract: this interface targets
35
- * the generic record-shaped storage layer (`InMemoryObjectStore.put`
36
- * + the IDB ObjectStore equivalent), both of which take
37
- * `Record<string, unknown>`. Every declared field below already
38
- * satisfies `unknown`; the index signature just makes the
39
- * interface assignable to the storage parameter without a cast. */
40
- interface PersistedTransaction {
41
- id: string;
42
- type?: string;
43
- timestamp?: number;
44
- createdAt?: number;
45
- mutations?: PersistedMutation[];
46
- awaitingDelta?: {
47
- syncIdNeeded: number;
48
- modelName: string;
49
- modelId: string;
50
- operationType: string;
51
- };
52
- [key: string]: unknown;
53
- }
54
21
  export type { BootstrapType };
55
22
  export interface BootstrapRequirements {
56
23
  type: BootstrapType;
@@ -15,63 +15,7 @@ import { persistenceDatabaseNamesForDeletion, purgeIndexedDbPersistence, } from
15
15
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
16
16
  import { logPositionSchema } from './logPosition.js';
17
17
  import { highestPersistedPrefixSyncId } from './sync/persistedPrefix.js';
18
- /**
19
- * Request identity excludes local timing metadata for re-entrant seals: a
20
- * retry rebuilds its envelope with a fresh `sequence`/seal clock, so comparing
21
- * those volatile fields would reject every legitimate same-request re-seal as
22
- * an idempotency conflict. Only the fields that define the wire request count.
23
- */
24
- function isSameOutboxRecord(existing, candidate) {
25
- if (existing.type === 'http_commit_envelope' &&
26
- candidate.type === 'http_commit_envelope') {
27
- const identity = (record) => ({
28
- id: record.id,
29
- type: record.type,
30
- storageVersion: record.storageVersion,
31
- idempotencyKey: record.idempotencyKey,
32
- // HTTP outbox rows written before protocol versioning are v1. Normalize
33
- // them so a same-request re-seal remains idempotent after an upgrade.
34
- protocolVersion: record.protocolVersion ?? 1,
35
- request: record.request,
36
- scopeNamespace: record.scopeNamespace,
37
- });
38
- if (existing.correlationId !== undefined &&
39
- candidate.correlationId !== undefined &&
40
- existing.correlationId !== candidate.correlationId) {
41
- return false;
42
- }
43
- return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
44
- }
45
- if (existing.type === 'commit_envelope' &&
46
- candidate.type === 'commit_envelope') {
47
- const identity = (record) => ({
48
- id: record.id,
49
- type: record.type,
50
- storageVersion: record.storageVersion,
51
- origin: record.origin,
52
- idempotencyKey: record.idempotencyKey,
53
- operations: record.operations,
54
- sourceMutationIds: record.sourceMutationIds,
55
- commitOptions: record.commitOptions,
56
- scope: record.scope,
57
- });
58
- if (existing.correlationId !== undefined &&
59
- candidate.correlationId !== undefined &&
60
- existing.correlationId !== candidate.correlationId) {
61
- return false;
62
- }
63
- return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
64
- }
65
- return JSON.stringify(existing) === JSON.stringify(candidate);
66
- }
67
- function isAcceptedOutboxPromotion(existing, candidate) {
68
- return (existing !== undefined &&
69
- (existing.type === 'commit_envelope' ||
70
- existing.type === 'http_commit_envelope') &&
71
- existing.type === candidate.type &&
72
- existing.acceptedAt === undefined &&
73
- candidate.acceptedAt !== undefined);
74
- }
18
+ import { isAcceptedOutboxPromotion, isSameOutboxRecord, } from './transactions/persistedTransaction.js';
75
19
  export class Database {
76
20
  // Core database components
77
21
  databaseManager;
@@ -937,24 +937,6 @@ export class Model {
937
937
  }
938
938
  // Try to get model class by identifier
939
939
  let ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
940
- // If not found by registered name, try mapping to the class name
941
- if (!ModelClass) {
942
- const classNameMap = {
943
- Task: 'TaskModel',
944
- Project: 'Project',
945
- Comment: 'CommentModel',
946
- User: 'UserModel',
947
- Organization: 'OrganizationModel',
948
- StatusGroup: 'StatusGroupModel',
949
- Team: 'TeamModel',
950
- Member: 'MemberModel',
951
- Role: 'RoleModel',
952
- };
953
- const className = classNameMap[modelIdentifier];
954
- if (className) {
955
- ModelClass = getActiveRegistry().getModelByName(className);
956
- }
957
- }
958
940
  if (!ModelClass) {
959
941
  throw new AbloValidationError(`Model class not found for: ${modelIdentifier}`, { code: 'model_class_not_registered' });
960
942
  }
@@ -451,8 +451,6 @@ export declare class SyncClient extends EventEmitter {
451
451
  }[];
452
452
  };
453
453
  };
454
- unassignEntity(entityType: string, entityId: string): Promise<void>;
455
- reassignEntity(entityType: string, entityId: string, assigneeType: string, assigneeId: string, id?: string): Promise<void>;
456
454
  /**
457
455
  * Apply a batch of delta results from Database to the InstanceCache.
458
456
  * Owns: model creation, upsert, remove, archive, conflict resolution.
@@ -1048,8 +1048,7 @@ export class SyncClient extends EventEmitter {
1048
1048
  // only materializes on the rare force-accept branch, for its log line.
1049
1049
  const shouldForceAcceptServer = (serverData.deletedAt !== undefined && serverData.deletedAt !== null) ||
1050
1050
  (serverData.archivedAt !== undefined && serverData.archivedAt !== null) ||
1051
- serverData.isActive === false ||
1052
- (serverData.unassignedAt !== undefined && serverData.unassignedAt !== null);
1051
+ serverData.isActive === false;
1053
1052
  if (shouldForceAcceptServer) {
1054
1053
  this.runtime.logger.debug('Accepting server update - critical state change detected', {
1055
1054
  modelId: localModel.id,
@@ -1113,13 +1112,10 @@ export class SyncClient extends EventEmitter {
1113
1112
  if (serverData.archivedAt !== undefined) {
1114
1113
  critical.archivedAt = serverData.archivedAt;
1115
1114
  }
1116
- // Deactivation states - critical for assignments and similar entities
1115
+ // Deactivation states are always critical.
1117
1116
  if (serverData.isActive !== undefined && serverData.isActive === false) {
1118
1117
  critical.isActive = false;
1119
1118
  }
1120
- if (serverData.unassignedAt !== undefined) {
1121
- critical.unassignedAt = serverData.unassignedAt;
1122
- }
1123
1119
  return critical;
1124
1120
  }
1125
1121
  /**
@@ -1475,19 +1471,6 @@ export class SyncClient extends EventEmitter {
1475
1471
  mutationQueue: this.mutationQueue.getDebugInfo(),
1476
1472
  };
1477
1473
  }
1478
- // --- Best-practice assignment ops ---
1479
- async unassignEntity(entityType, entityId) {
1480
- // Call server-side unassign to avoid per-id races
1481
- await this.mutationExecutor.executeDelete('Assignment', entityId);
1482
- }
1483
- async reassignEntity(entityType, entityId, assigneeType, assigneeId, id) {
1484
- await this.mutationExecutor.executeCreate('Assignment', id || '', {
1485
- entityType,
1486
- entityId,
1487
- assigneeType,
1488
- assigneeId,
1489
- });
1490
- }
1491
1474
  // ── Delta + Bootstrap application (owns InstanceCache writes) ──────────────
1492
1475
  /**
1493
1476
  * Apply a batch of delta results from Database to the InstanceCache.
@@ -75,7 +75,7 @@ hydration, collaboration, readSetContext) {
75
75
  'but no matching constructor was registered.', { code: 'model_not_registered' });
76
76
  }
77
77
  // The coordination plane must speak the same wire dialect as the commit
78
- // plane: the lowercased typename (`task`), not the schema key (`tasks`). The
78
+ // plane: the lowercased typename (`item`), not the schema key (`items`). The
79
79
  // server's commit-time claim guard probes the lease store with the commit
80
80
  // operation's model name, so a lease recorded under the schema key never
81
81
  // matches — which would silently disarm the guard for every model whose
@@ -253,13 +253,13 @@ export interface RuntimeConfig {
253
253
  * Fields to preserve when merging a partial update into the local store. A
254
254
  * change usually carries only the fields that changed; listing a model's
255
255
  * essential fields here keeps them from being dropped during that merge.
256
- * For example: `{ Task: ['title', 'projectId'], Section: ['reportId', 'order'] }`.
256
+ * For example: `{ Item: ['title', 'projectId'], Section: ['reportId', 'order'] }`.
257
257
  */
258
258
  essentialFields: Readonly<Record<string, readonly string[]>>;
259
259
  /**
260
260
  * A fallback map from class name to model name, used to resolve a model's name
261
261
  * when the usual lookup fails — for instance, when a bundler has minified the
262
- * class names. For example: `{ TaskModel: 'Task', ProjectModel: 'Project' }`.
262
+ * class names. For example: `{ ItemModel: 'Item', ProjectModel: 'Project' }`.
263
263
  */
264
264
  classNameFallbackMap: Readonly<Record<string, string>>;
265
265
  /**
@@ -283,7 +283,7 @@ export interface RuntimeConfig {
283
283
  expectedSourceSchemaHash?: string;
284
284
  /**
285
285
  * Per-model content hashes of the schema this client was built against,
286
- * keyed by schema key (`tasks` → hash of that model's serialized JSON). The
286
+ * keyed by schema key (`items` → hash of that model's serialized JSON). The
287
287
  * semantic layer of the drift check: on a whole-schema mismatch the client
288
288
  * compares only the models IT declares against the server's per-model
289
289
  * surface, so a purely additive server-side change (new models this build
@@ -415,7 +415,7 @@ export class OnDemandLoader {
415
415
  // that disagrees with the schema's: these rows were returned FOR this
416
416
  // model's query, so the schema typename is correct by construction — and
417
417
  // without stripping it, the spread would put the row's variant (a server
418
- // echoing the schema KEY `tasks` instead of the typename `Task`) back on
418
+ // echoing the schema KEY `items` instead of the typename `Item`) back on
419
419
  // top of the stamp, sending hydration to the strict unknown-model error.
420
420
  const { _Typename: _dropMangled, __typename: _dropRowVariant, ...rest } = obj;
421
421
  void _dropMangled;
@@ -276,7 +276,7 @@ async function drainPendingDeltas(ctx) {
276
276
  // A sustained stream can refill the detached queue before every
277
277
  // persistence promise settles. Promise-only looping then forms an
278
278
  // unbounded microtask chain that starves WebSocket reads, timers and
279
- // replication keepalives. Give the host one macrotask turn between
279
+ // replication keepalives. Give the host one macroitem turn between
280
280
  // owned batches; Node has setImmediate, browsers fall back to a timer.
281
281
  await yieldToHost();
282
282
  }
@@ -43,7 +43,7 @@ export function* initialize(host, context, signal) {
43
43
  // Bootstrap from server if needed.
44
44
  //
45
45
  // `bootstrapMode: 'none'` participants (headless workers and
46
- // task runners) skip baseline replication — they read via
46
+ // item runners) skip baseline replication — they read via
47
47
  // `model.get()` round-trips and rely on covering deltas
48
48
  // from filtered subscriptions to populate the pool lazily. The
49
49
  // WS is already open by `setupWebSocketSync` above, so live
@@ -55,7 +55,7 @@ export function* initialize(host, context, signal) {
55
55
  // initiates the upgrade, but it does NOT await the 'connected'
56
56
  // event — it returns synchronously after wiring listeners.
57
57
  // For bootstrapMode='none' consumers (headless workers and
58
- // task runners), this branch is the entire body of initialize()
58
+ // item runners), this branch is the entire body of initialize()
59
59
  // after the WS is set up, so `ready()` would otherwise resolve
60
60
  // while the WS is still in 'connecting' state. The very next
61
61
  // `commits.create` then throws "SyncWebSocket not connected".
@@ -167,7 +167,7 @@ export declare class MutationQueue extends EventEmitter {
167
167
  /**
168
168
  * Relates stale notifications back to write targets without assuming the
169
169
  * server's canonical model name uses the same spelling as the public schema
170
- * key (`Task` versus `tasks`). Exact `(model,id)` wins; a globally unique id
170
+ * key (`Item` versus `items`). Exact `(model,id)` wins; a globally unique id
171
171
  * is the compatibility fallback. An ambiguous same-id cross-model mismatch
172
172
  * is deliberately left unclassified, so it cannot falsely settle a queued
173
173
  * write. A notification with no write-target id (or an explicit group) is a
@@ -417,7 +417,7 @@ export class MutationQueue extends EventEmitter {
417
417
  /**
418
418
  * Relates stale notifications back to write targets without assuming the
419
419
  * server's canonical model name uses the same spelling as the public schema
420
- * key (`Task` versus `tasks`). Exact `(model,id)` wins; a globally unique id
420
+ * key (`Item` versus `items`). Exact `(model,id)` wins; a globally unique id
421
421
  * is the compatibility fallback. An ambiguous same-id cross-model mismatch
422
422
  * is deliberately left unclassified, so it cannot falsely settle a queued
423
423
  * write. A notification with no write-target id (or an explicit group) is a
@@ -0,0 +1,39 @@
1
+ import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
2
+ type ModelData = Record<string, unknown>;
3
+ /** One mutation retained in the durable local transaction journal. */
4
+ interface PersistedMutation {
5
+ type: 'create' | 'update' | 'delete' | 'archive';
6
+ modelData: ModelData;
7
+ modelName: string;
8
+ timestamp: string;
9
+ writeOptions?: {
10
+ readAt?: number | null;
11
+ onStale?: OnStaleMode | null;
12
+ };
13
+ }
14
+ /**
15
+ * Persisted transaction for offline/retry support.
16
+ *
17
+ * The index signature is part of the contract: this targets the generic
18
+ * record-shaped storage layer (`InMemoryObjectStore.put` and its IndexedDB
19
+ * equivalent), both of which take `Record<string, unknown>`.
20
+ */
21
+ export interface PersistedTransaction {
22
+ id: string;
23
+ type?: string;
24
+ timestamp?: number;
25
+ createdAt?: number;
26
+ mutations?: PersistedMutation[];
27
+ awaitingDelta?: {
28
+ syncIdNeeded: number;
29
+ modelName: string;
30
+ modelId: string;
31
+ operationType: string;
32
+ };
33
+ [key: string]: unknown;
34
+ }
35
+ /** Compare the stable request identity while ignoring local seal timing. */
36
+ export declare function isSameOutboxRecord(existing: PersistedTransaction, candidate: PersistedTransaction): boolean;
37
+ /** An accepted envelope may replace the otherwise-identical pending envelope. */
38
+ export declare function isAcceptedOutboxPromotion(existing: PersistedTransaction | undefined, candidate: PersistedTransaction): boolean;
39
+ export {};
@@ -0,0 +1,53 @@
1
+ /** Compare the stable request identity while ignoring local seal timing. */
2
+ export function isSameOutboxRecord(existing, candidate) {
3
+ if (existing.type === 'http_commit_envelope' &&
4
+ candidate.type === 'http_commit_envelope') {
5
+ const identity = (record) => ({
6
+ id: record.id,
7
+ type: record.type,
8
+ storageVersion: record.storageVersion,
9
+ idempotencyKey: record.idempotencyKey,
10
+ // Pre-versioning HTTP outbox rows are v1. Normalizing them preserves
11
+ // idempotency when the same request is resealed after an upgrade.
12
+ protocolVersion: record.protocolVersion ?? 1,
13
+ request: record.request,
14
+ scopeNamespace: record.scopeNamespace,
15
+ });
16
+ if (existing.correlationId !== undefined &&
17
+ candidate.correlationId !== undefined &&
18
+ existing.correlationId !== candidate.correlationId) {
19
+ return false;
20
+ }
21
+ return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
22
+ }
23
+ if (existing.type === 'commit_envelope' &&
24
+ candidate.type === 'commit_envelope') {
25
+ const identity = (record) => ({
26
+ id: record.id,
27
+ type: record.type,
28
+ storageVersion: record.storageVersion,
29
+ origin: record.origin,
30
+ idempotencyKey: record.idempotencyKey,
31
+ operations: record.operations,
32
+ sourceMutationIds: record.sourceMutationIds,
33
+ commitOptions: record.commitOptions,
34
+ scope: record.scope,
35
+ });
36
+ if (existing.correlationId !== undefined &&
37
+ candidate.correlationId !== undefined &&
38
+ existing.correlationId !== candidate.correlationId) {
39
+ return false;
40
+ }
41
+ return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
42
+ }
43
+ return JSON.stringify(existing) === JSON.stringify(candidate);
44
+ }
45
+ /** An accepted envelope may replace the otherwise-identical pending envelope. */
46
+ export function isAcceptedOutboxPromotion(existing, candidate) {
47
+ return (existing !== undefined &&
48
+ (existing.type === 'commit_envelope' ||
49
+ existing.type === 'http_commit_envelope') &&
50
+ existing.type === candidate.type &&
51
+ existing.acceptedAt === undefined &&
52
+ candidate.acceptedAt !== undefined);
53
+ }
@@ -48,7 +48,7 @@ export function M1(target, propertyMetadata, referenceMetadata) {
48
48
  return false;
49
49
  };
50
50
  // Skip if target has its own observability setup
51
- // This allows models like Task to handle their own MobX setup
51
+ // This allows models like Item to handle their own MobX setup
52
52
  if (target.setupObservability || target._hasCustomObservability) {
53
53
  getContext().modelDebugLogger?.logDebug(`${target.constructor.name} has custom observability, skipping M1`);
54
54
  return;
@@ -206,10 +206,10 @@ export declare function usePeers(scope?: ParticipantScope): readonly Peer[];
206
206
  /**
207
207
  * Returns the raw `SyncEngine` proxy. Typically you want the typed
208
208
  * hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
209
- * where you need direct access (e.g., `sync.tasks.onChange(cb)`).
209
+ * where you need direct access (e.g., `sync.items.onChange(cb)`).
210
210
  *
211
211
  * The generic parameter narrows the return type to your schema's
212
- * model record so call sites get typed `sync.tasks.findMany()` /
212
+ * model record so call sites get typed `sync.items.findMany()` /
213
213
  * `sync.sections.create(...)` without a cast at the call site:
214
214
  *
215
215
  * ```ts
@@ -400,10 +400,10 @@ export function usePeers(scope) {
400
400
  /**
401
401
  * Returns the raw `SyncEngine` proxy. Typically you want the typed
402
402
  * hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
403
- * where you need direct access (e.g., `sync.tasks.onChange(cb)`).
403
+ * where you need direct access (e.g., `sync.items.onChange(cb)`).
404
404
  *
405
405
  * The generic parameter narrows the return type to your schema's
406
- * model record so call sites get typed `sync.tasks.findMany()` /
406
+ * model record so call sites get typed `sync.items.findMany()` /
407
407
  * `sync.sections.create(...)` without a cast at the call site:
408
408
  *
409
409
  * ```ts
@@ -8,7 +8,7 @@ export interface SyncReactContext {
8
8
  organizationId: string;
9
9
  /**
10
10
  * An optional schema. When provided, hooks that take a model by name (such as
11
- * `useQuery('tasks')`) read that model's metadata from this schema, so
11
+ * `useQuery('items')`) read that model's metadata from this schema, so
12
12
  * callers don't pass a schema at every call site. When omitted, those hooks
13
13
  * require the schema as an argument instead.
14
14
  *
@@ -38,7 +38,7 @@ export interface SyncProviderProps {
38
38
  organizationId: string;
39
39
  /**
40
40
  * An optional schema. Provide it to enable hooks that take a model by name
41
- * (such as `useQuery('tasks')`); the model types also narrow through your
41
+ * (such as `useQuery('items')`); the model types also narrow through your
42
42
  * `Register` augmentation. Omit it to pass the schema to those hooks directly
43
43
  * instead.
44
44
  */
@@ -54,13 +54,13 @@ export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'>
54
54
  * // With the Register augmentation (recommended):
55
55
  * const ablo = useAblo();
56
56
  * if (!ablo) return <Loading />;
57
- * const doc = await ablo.documents.get({ id }); // async server read
57
+ * const doc = await ablo.records.get({ id }); // async server read
58
58
  *
59
59
  * // Reactive selector (a synchronous local snapshot). The selector's reads
60
60
  * // are typed as snapshot rows — data fields + computeds, no relation
61
61
  * // accessors — matching what the hook actually returns:
62
- * const doc = useAblo((ablo) => ablo.documents.local.get(id)) ?? serverDoc;
63
- * const active = useAblo((ablo) => ablo.documents.claim.state({ id }));
62
+ * const doc = useAblo((ablo) => ablo.records.local.get(id)) ?? serverDoc;
63
+ * const active = useAblo((ablo) => ablo.records.claim.state({ id }));
64
64
  *
65
65
  * // Without the augmentation, pass the schema as a type argument:
66
66
  * const ablo = useAblo<(typeof schema)['models']>();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/humans",
3
- "version": "0.51.0",
3
+ "version": "0.52.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",
@@ -84,7 +84,7 @@
84
84
  "directory": "packages/humans"
85
85
  },
86
86
  "dependencies": {
87
- "@abloatai/transaction": "^0.51.0",
87
+ "@abloatai/transaction": "^0.52.0",
88
88
  "mobx": "^6.13.7",
89
89
  "uuid": "^11.1.0",
90
90
  "zod": "^4.4.3"
package/src/client.ts CHANGED
@@ -131,10 +131,10 @@ export type AbloClient<S extends SchemaRecord> = {
131
131
  * server verifies it. The browser must never see the `sk_` key, only the
132
132
  * per-user session token.
133
133
  *
134
- * Pass `{ user: { id }, can: { tasks: ['read', 'update'] } }` for an end-user
134
+ * Pass `{ user: { id }, can: { items: ['read', 'update'] } }` for an end-user
135
135
  * session. It mints an `ek_` and attributes writes to a user (recorded as
136
136
  * `actor_kind` on the delta row). Pass `{ agent: { id }, can: {
137
- * tasks: ['update'] } }` for a scoped agent session, which mints an `rk_`.
137
+ * items: ['update'] } }` for a scoped agent session, which mints an `rk_`.
138
138
  * Both kinds require `can`, typed against your schema's model names. This
139
139
  * always authenticates with the original `sk_`, never the client's exchanged
140
140
  * sync credential.
@@ -150,10 +150,10 @@ export type AbloClient<S extends SchemaRecord> = {
150
150
  * ```ts
151
151
  * const agent = await ablo.agents.create({
152
152
  * name: 'researcher', // readable label (optional)
153
- * can: { documents: ['read', 'update'] },
153
+ * can: { records: ['read', 'update'] },
154
154
  * // id omitted → a fresh uuid: a distinct, independent participant
155
155
  * });
156
- * await agent.documents.update({ id, data, claim });
156
+ * await agent.records.update({ id, data, claim });
157
157
  * await agent.dispose(); // when the agent is done
158
158
  * ```
159
159
  *
@@ -84,7 +84,7 @@ import type { CommitLatencySample } from './transactions/mutations/commitLatency
84
84
  export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
85
85
 
86
86
  /** Concrete constructor type for instantiation */
87
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Constructor args vary per model (PrismaTask, Record<string, unknown>, etc.)
87
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Constructor args vary per model (PrismaItem, Record<string, unknown>, etc.)
88
88
  export type ConcreteModelConstructor<T extends Model> = new (data?: any) => T;
89
89
 
90
90
  // ModelData is defined in a separate module to break the type cycle between
@@ -23,125 +23,17 @@ import type { BootstrapFetcher, BootstrapData } from './sync/BootstrapFetcher.js
23
23
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
24
24
  import { logPositionSchema } from './logPosition.js';
25
25
  import type { SyncDeltaAction } from '@abloatai/transaction/wire/delta';
26
- import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
27
26
  import type { BootstrapType } from '@abloatai/transaction/types';
28
27
  import { highestPersistedPrefixSyncId } from './sync/persistedPrefix.js';
28
+ import {
29
+ isAcceptedOutboxPromotion,
30
+ isSameOutboxRecord,
31
+ type PersistedTransaction,
32
+ } from './transactions/persistedTransaction.js';
29
33
 
30
34
  /** Generic record type for model data */
31
35
  type ModelData = Record<string, unknown>;
32
36
 
33
- /** Persisted mutation in a transaction */
34
- interface PersistedMutation {
35
- type: 'create' | 'update' | 'delete' | 'archive';
36
- modelData: ModelData;
37
- modelName: string;
38
- timestamp: string;
39
- writeOptions?: {
40
- readAt?: number | null;
41
- onStale?: OnStaleMode | null;
42
- };
43
- }
44
-
45
- /** Persisted transaction for offline/retry support.
46
- *
47
- * Index signature is part of the contract: this interface targets
48
- * the generic record-shaped storage layer (`InMemoryObjectStore.put`
49
- * + the IDB ObjectStore equivalent), both of which take
50
- * `Record<string, unknown>`. Every declared field below already
51
- * satisfies `unknown`; the index signature just makes the
52
- * interface assignable to the storage parameter without a cast. */
53
- interface PersistedTransaction {
54
- id: string;
55
- type?: string;
56
- timestamp?: number;
57
- createdAt?: number;
58
- mutations?: PersistedMutation[];
59
- // Persist awaiting-delta transactions so they survive a tab close. On the
60
- // next session, WebSocket reconnect plus delta catch-up confirms them.
61
- awaitingDelta?: {
62
- syncIdNeeded: number;
63
- modelName: string;
64
- modelId: string;
65
- operationType: string;
66
- };
67
- [key: string]: unknown;
68
- }
69
-
70
- /**
71
- * Request identity excludes local timing metadata for re-entrant seals: a
72
- * retry rebuilds its envelope with a fresh `sequence`/seal clock, so comparing
73
- * those volatile fields would reject every legitimate same-request re-seal as
74
- * an idempotency conflict. Only the fields that define the wire request count.
75
- */
76
- function isSameOutboxRecord(
77
- existing: PersistedTransaction,
78
- candidate: PersistedTransaction,
79
- ): boolean {
80
- if (
81
- existing.type === 'http_commit_envelope' &&
82
- candidate.type === 'http_commit_envelope'
83
- ) {
84
- const identity = (record: PersistedTransaction): unknown => ({
85
- id: record.id,
86
- type: record.type,
87
- storageVersion: record.storageVersion,
88
- idempotencyKey: record.idempotencyKey,
89
- // HTTP outbox rows written before protocol versioning are v1. Normalize
90
- // them so a same-request re-seal remains idempotent after an upgrade.
91
- protocolVersion: record.protocolVersion ?? 1,
92
- request: record.request,
93
- scopeNamespace: record.scopeNamespace,
94
- });
95
- if (
96
- existing.correlationId !== undefined &&
97
- candidate.correlationId !== undefined &&
98
- existing.correlationId !== candidate.correlationId
99
- ) {
100
- return false;
101
- }
102
- return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
103
- }
104
- if (
105
- existing.type === 'commit_envelope' &&
106
- candidate.type === 'commit_envelope'
107
- ) {
108
- const identity = (record: PersistedTransaction): unknown => ({
109
- id: record.id,
110
- type: record.type,
111
- storageVersion: record.storageVersion,
112
- origin: record.origin,
113
- idempotencyKey: record.idempotencyKey,
114
- operations: record.operations,
115
- sourceMutationIds: record.sourceMutationIds,
116
- commitOptions: record.commitOptions,
117
- scope: record.scope,
118
- });
119
- if (
120
- existing.correlationId !== undefined &&
121
- candidate.correlationId !== undefined &&
122
- existing.correlationId !== candidate.correlationId
123
- ) {
124
- return false;
125
- }
126
- return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
127
- }
128
- return JSON.stringify(existing) === JSON.stringify(candidate);
129
- }
130
-
131
- function isAcceptedOutboxPromotion(
132
- existing: PersistedTransaction | undefined,
133
- candidate: PersistedTransaction,
134
- ): boolean {
135
- return (
136
- existing !== undefined &&
137
- (existing.type === 'commit_envelope' ||
138
- existing.type === 'http_commit_envelope') &&
139
- existing.type === candidate.type &&
140
- existing.acceptedAt === undefined &&
141
- candidate.acceptedAt !== undefined
142
- );
143
- }
144
-
145
37
  // Re-exported, not redeclared. `@abloatai/transaction`'s `types` module owns this
146
38
  // vocabulary and documents what each mode does; this package held a byte-identical
147
39
  // second copy while its own test fixtures already imported the canonical one.
@@ -1108,26 +1108,6 @@ export abstract class Model {
1108
1108
  // Try to get model class by identifier
1109
1109
  let ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
1110
1110
 
1111
- // If not found by registered name, try mapping to the class name
1112
- if (!ModelClass) {
1113
- const classNameMap: Record<string, string> = {
1114
- Task: 'TaskModel',
1115
- Project: 'Project',
1116
- Comment: 'CommentModel',
1117
- User: 'UserModel',
1118
- Organization: 'OrganizationModel',
1119
- StatusGroup: 'StatusGroupModel',
1120
- Team: 'TeamModel',
1121
- Member: 'MemberModel',
1122
- Role: 'RoleModel',
1123
- };
1124
-
1125
- const className = classNameMap[modelIdentifier];
1126
- if (className) {
1127
- ModelClass = getActiveRegistry().getModelByName(className);
1128
- }
1129
- }
1130
-
1131
1111
  if (!ModelClass) {
1132
1112
  throw new AbloValidationError(
1133
1113
  `Model class not found for: ${modelIdentifier}`,
@@ -1302,8 +1302,7 @@ export class SyncClient extends EventEmitter {
1302
1302
  const shouldForceAcceptServer =
1303
1303
  (serverData.deletedAt !== undefined && serverData.deletedAt !== null) ||
1304
1304
  (serverData.archivedAt !== undefined && serverData.archivedAt !== null) ||
1305
- serverData.isActive === false ||
1306
- (serverData.unassignedAt !== undefined && serverData.unassignedAt !== null);
1305
+ serverData.isActive === false;
1307
1306
 
1308
1307
  if (shouldForceAcceptServer) {
1309
1308
  this.runtime.logger.debug('Accepting server update - critical state change detected', {
@@ -1378,14 +1377,10 @@ export class SyncClient extends EventEmitter {
1378
1377
  critical.archivedAt = serverData.archivedAt;
1379
1378
  }
1380
1379
 
1381
- // Deactivation states - critical for assignments and similar entities
1380
+ // Deactivation states are always critical.
1382
1381
  if (serverData.isActive !== undefined && serverData.isActive === false) {
1383
1382
  critical.isActive = false;
1384
1383
  }
1385
- if (serverData.unassignedAt !== undefined) {
1386
- critical.unassignedAt = serverData.unassignedAt;
1387
- }
1388
-
1389
1384
  return critical;
1390
1385
  }
1391
1386
 
@@ -1824,27 +1819,6 @@ export class SyncClient extends EventEmitter {
1824
1819
  };
1825
1820
  }
1826
1821
 
1827
- // --- Best-practice assignment ops ---
1828
- async unassignEntity(entityType: string, entityId: string): Promise<void> {
1829
- // Call server-side unassign to avoid per-id races
1830
- await this.mutationExecutor.executeDelete('Assignment', entityId);
1831
- }
1832
-
1833
- async reassignEntity(
1834
- entityType: string,
1835
- entityId: string,
1836
- assigneeType: string,
1837
- assigneeId: string,
1838
- id?: string
1839
- ): Promise<void> {
1840
- await this.mutationExecutor.executeCreate('Assignment', id || '', {
1841
- entityType,
1842
- entityId,
1843
- assigneeType,
1844
- assigneeId,
1845
- });
1846
- }
1847
-
1848
1822
  // ── Delta + Bootstrap application (owns InstanceCache writes) ──────────────
1849
1823
 
1850
1824
  /**
@@ -159,7 +159,7 @@ type EntityHalf = Pick<ModelTarget, 'model' | 'id'>;
159
159
  // Model-agnostic by construction: every member below names a target by
160
160
  // `{ model, id }` and answers in claim/snapshot terms, so the row type never
161
161
  // appears. It carried a `<T>` that nothing in the body read, which made
162
- // `ModelCollaboration<Task>` and `ModelCollaboration<Invoice>` the same type
162
+ // `ModelCollaboration<Item>` and `ModelCollaboration<Invoice>` the same type
163
163
  // while reading as though they differed.
164
164
  export interface ModelCollaboration {
165
165
  /** Exact point evidence from the HTTP read boundary (stamp captured before data). */
@@ -458,7 +458,7 @@ export function createModelProxy<T, C>(
458
458
  }
459
459
 
460
460
  // The coordination plane must speak the same wire dialect as the commit
461
- // plane: the lowercased typename (`task`), not the schema key (`tasks`). The
461
+ // plane: the lowercased typename (`item`), not the schema key (`items`). The
462
462
  // server's commit-time claim guard probes the lease store with the commit
463
463
  // operation's model name, so a lease recorded under the schema key never
464
464
  // matches — which would silently disarm the guard for every model whose
@@ -396,14 +396,14 @@ export interface RuntimeConfig {
396
396
  * Fields to preserve when merging a partial update into the local store. A
397
397
  * change usually carries only the fields that changed; listing a model's
398
398
  * essential fields here keeps them from being dropped during that merge.
399
- * For example: `{ Task: ['title', 'projectId'], Section: ['reportId', 'order'] }`.
399
+ * For example: `{ Item: ['title', 'projectId'], Section: ['reportId', 'order'] }`.
400
400
  */
401
401
  essentialFields: Readonly<Record<string, readonly string[]>>;
402
402
 
403
403
  /**
404
404
  * A fallback map from class name to model name, used to resolve a model's name
405
405
  * when the usual lookup fails — for instance, when a bundler has minified the
406
- * class names. For example: `{ TaskModel: 'Task', ProjectModel: 'Project' }`.
406
+ * class names. For example: `{ ItemModel: 'Item', ProjectModel: 'Project' }`.
407
407
  */
408
408
  classNameFallbackMap: Readonly<Record<string, string>>;
409
409
 
@@ -430,7 +430,7 @@ export interface RuntimeConfig {
430
430
 
431
431
  /**
432
432
  * Per-model content hashes of the schema this client was built against,
433
- * keyed by schema key (`tasks` → hash of that model's serialized JSON). The
433
+ * keyed by schema key (`items` → hash of that model's serialized JSON). The
434
434
  * semantic layer of the drift check: on a whole-schema mismatch the client
435
435
  * compares only the models IT declares against the server's per-model
436
436
  * surface, so a purely additive server-side change (new models this build
@@ -556,7 +556,7 @@ export class OnDemandLoader {
556
556
  // that disagrees with the schema's: these rows were returned FOR this
557
557
  // model's query, so the schema typename is correct by construction — and
558
558
  // without stripping it, the spread would put the row's variant (a server
559
- // echoing the schema KEY `tasks` instead of the typename `Task`) back on
559
+ // echoing the schema KEY `items` instead of the typename `Item`) back on
560
560
  // top of the stamp, sending hydration to the strict unknown-model error.
561
561
  const { _Typename: _dropMangled, __typename: _dropRowVariant, ...rest } = obj as Record<
562
562
  string,
@@ -396,7 +396,7 @@ async function drainPendingDeltas(ctx: DeltaPipelineContext): Promise<void> {
396
396
  // A sustained stream can refill the detached queue before every
397
397
  // persistence promise settles. Promise-only looping then forms an
398
398
  // unbounded microtask chain that starves WebSocket reads, timers and
399
- // replication keepalives. Give the host one macrotask turn between
399
+ // replication keepalives. Give the host one macroitem turn between
400
400
  // owned batches; Node has setImmediate, browsers fall back to a timer.
401
401
  await yieldToHost();
402
402
  }
@@ -92,7 +92,7 @@ export function* initialize<TCollaboration extends EventMap<TCollaboration>>(
92
92
  // Bootstrap from server if needed.
93
93
  //
94
94
  // `bootstrapMode: 'none'` participants (headless workers and
95
- // task runners) skip baseline replication — they read via
95
+ // item runners) skip baseline replication — they read via
96
96
  // `model.get()` round-trips and rely on covering deltas
97
97
  // from filtered subscriptions to populate the pool lazily. The
98
98
  // WS is already open by `setupWebSocketSync` above, so live
@@ -110,7 +110,7 @@ export function* initialize<TCollaboration extends EventMap<TCollaboration>>(
110
110
  // initiates the upgrade, but it does NOT await the 'connected'
111
111
  // event — it returns synchronously after wiring listeners.
112
112
  // For bootstrapMode='none' consumers (headless workers and
113
- // task runners), this branch is the entire body of initialize()
113
+ // item runners), this branch is the entire body of initialize()
114
114
  // after the WS is set up, so `ready()` would otherwise resolve
115
115
  // while the WS is still in 'connecting' state. The very next
116
116
  // `commits.create` then throws "SyncWebSocket not connected".
@@ -645,7 +645,7 @@ export class MutationQueue extends EventEmitter {
645
645
  /**
646
646
  * Relates stale notifications back to write targets without assuming the
647
647
  * server's canonical model name uses the same spelling as the public schema
648
- * key (`Task` versus `tasks`). Exact `(model,id)` wins; a globally unique id
648
+ * key (`Item` versus `items`). Exact `(model,id)` wins; a globally unique id
649
649
  * is the compatibility fallback. An ambiguous same-id cross-model mismatch
650
650
  * is deliberately left unclassified, so it cannot falsely settle a queued
651
651
  * write. A notification with no write-target id (or an explicit group) is a
@@ -0,0 +1,112 @@
1
+ import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
2
+
3
+ type ModelData = Record<string, unknown>;
4
+
5
+ /** One mutation retained in the durable local transaction journal. */
6
+ interface PersistedMutation {
7
+ type: 'create' | 'update' | 'delete' | 'archive';
8
+ modelData: ModelData;
9
+ modelName: string;
10
+ timestamp: string;
11
+ writeOptions?: {
12
+ readAt?: number | null;
13
+ onStale?: OnStaleMode | null;
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Persisted transaction for offline/retry support.
19
+ *
20
+ * The index signature is part of the contract: this targets the generic
21
+ * record-shaped storage layer (`InMemoryObjectStore.put` and its IndexedDB
22
+ * equivalent), both of which take `Record<string, unknown>`.
23
+ */
24
+ export interface PersistedTransaction {
25
+ id: string;
26
+ type?: string;
27
+ timestamp?: number;
28
+ createdAt?: number;
29
+ mutations?: PersistedMutation[];
30
+ // Awaiting-delta transactions survive a tab close. Reconnect and delta
31
+ // catch-up confirm them during the next session.
32
+ awaitingDelta?: {
33
+ syncIdNeeded: number;
34
+ modelName: string;
35
+ modelId: string;
36
+ operationType: string;
37
+ };
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ /** Compare the stable request identity while ignoring local seal timing. */
42
+ export function isSameOutboxRecord(
43
+ existing: PersistedTransaction,
44
+ candidate: PersistedTransaction,
45
+ ): boolean {
46
+ if (
47
+ existing.type === 'http_commit_envelope' &&
48
+ candidate.type === 'http_commit_envelope'
49
+ ) {
50
+ const identity = (record: PersistedTransaction): unknown => ({
51
+ id: record.id,
52
+ type: record.type,
53
+ storageVersion: record.storageVersion,
54
+ idempotencyKey: record.idempotencyKey,
55
+ // Pre-versioning HTTP outbox rows are v1. Normalizing them preserves
56
+ // idempotency when the same request is resealed after an upgrade.
57
+ protocolVersion: record.protocolVersion ?? 1,
58
+ request: record.request,
59
+ scopeNamespace: record.scopeNamespace,
60
+ });
61
+ if (
62
+ existing.correlationId !== undefined &&
63
+ candidate.correlationId !== undefined &&
64
+ existing.correlationId !== candidate.correlationId
65
+ ) {
66
+ return false;
67
+ }
68
+ return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
69
+ }
70
+
71
+ if (
72
+ existing.type === 'commit_envelope' &&
73
+ candidate.type === 'commit_envelope'
74
+ ) {
75
+ const identity = (record: PersistedTransaction): unknown => ({
76
+ id: record.id,
77
+ type: record.type,
78
+ storageVersion: record.storageVersion,
79
+ origin: record.origin,
80
+ idempotencyKey: record.idempotencyKey,
81
+ operations: record.operations,
82
+ sourceMutationIds: record.sourceMutationIds,
83
+ commitOptions: record.commitOptions,
84
+ scope: record.scope,
85
+ });
86
+ if (
87
+ existing.correlationId !== undefined &&
88
+ candidate.correlationId !== undefined &&
89
+ existing.correlationId !== candidate.correlationId
90
+ ) {
91
+ return false;
92
+ }
93
+ return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
94
+ }
95
+
96
+ return JSON.stringify(existing) === JSON.stringify(candidate);
97
+ }
98
+
99
+ /** An accepted envelope may replace the otherwise-identical pending envelope. */
100
+ export function isAcceptedOutboxPromotion(
101
+ existing: PersistedTransaction | undefined,
102
+ candidate: PersistedTransaction,
103
+ ): boolean {
104
+ return (
105
+ existing !== undefined &&
106
+ (existing.type === 'commit_envelope' ||
107
+ existing.type === 'http_commit_envelope') &&
108
+ existing.type === candidate.type &&
109
+ existing.acceptedAt === undefined &&
110
+ candidate.acceptedAt !== undefined
111
+ );
112
+ }
@@ -77,7 +77,7 @@ export function M1<T extends M1Target>(
77
77
  };
78
78
 
79
79
  // Skip if target has its own observability setup
80
- // This allows models like Task to handle their own MobX setup
80
+ // This allows models like Item to handle their own MobX setup
81
81
  if (target.setupObservability || target._hasCustomObservability) {
82
82
  getContext().modelDebugLogger?.logDebug(`${target.constructor.name} has custom observability, skipping M1`);
83
83
  return;
@@ -675,10 +675,10 @@ export function usePeers(scope?: ParticipantScope): readonly Peer[] {
675
675
  /**
676
676
  * Returns the raw `SyncEngine` proxy. Typically you want the typed
677
677
  * hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
678
- * where you need direct access (e.g., `sync.tasks.onChange(cb)`).
678
+ * where you need direct access (e.g., `sync.items.onChange(cb)`).
679
679
  *
680
680
  * The generic parameter narrows the return type to your schema's
681
- * model record so call sites get typed `sync.tasks.findMany()` /
681
+ * model record so call sites get typed `sync.items.findMany()` /
682
682
  * `sync.sections.create(...)` without a cast at the call site:
683
683
  *
684
684
  * ```ts
@@ -19,7 +19,7 @@ export interface SyncReactContext {
19
19
  organizationId: string;
20
20
  /**
21
21
  * An optional schema. When provided, hooks that take a model by name (such as
22
- * `useQuery('tasks')`) read that model's metadata from this schema, so
22
+ * `useQuery('items')`) read that model's metadata from this schema, so
23
23
  * callers don't pass a schema at every call site. When omitted, those hooks
24
24
  * require the schema as an argument instead.
25
25
  *
@@ -60,7 +60,7 @@ export interface SyncProviderProps {
60
60
  organizationId: string;
61
61
  /**
62
62
  * An optional schema. Provide it to enable hooks that take a model by name
63
- * (such as `useQuery('tasks')`); the model types also narrow through your
63
+ * (such as `useQuery('items')`); the model types also narrow through your
64
64
  * `Register` augmentation. Omit it to pass the schema to those hooks directly
65
65
  * instead.
66
66
  */
@@ -138,13 +138,13 @@ function snapshotValue<T>(value: T): T {
138
138
  * // With the Register augmentation (recommended):
139
139
  * const ablo = useAblo();
140
140
  * if (!ablo) return <Loading />;
141
- * const doc = await ablo.documents.get({ id }); // async server read
141
+ * const doc = await ablo.records.get({ id }); // async server read
142
142
  *
143
143
  * // Reactive selector (a synchronous local snapshot). The selector's reads
144
144
  * // are typed as snapshot rows — data fields + computeds, no relation
145
145
  * // accessors — matching what the hook actually returns:
146
- * const doc = useAblo((ablo) => ablo.documents.local.get(id)) ?? serverDoc;
147
- * const active = useAblo((ablo) => ablo.documents.claim.state({ id }));
146
+ * const doc = useAblo((ablo) => ablo.records.local.get(id)) ?? serverDoc;
147
+ * const active = useAblo((ablo) => ablo.records.claim.state({ id }));
148
148
  *
149
149
  * // Without the augmentation, pass the schema as a type argument:
150
150
  * const ablo = useAblo<(typeof schema)['models']>();