zerodrift 1.0.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +358 -0
  3. package/dist/core/BaseModel.d.ts +76 -0
  4. package/dist/core/BaseModel.js +505 -0
  5. package/dist/core/BaseSSEConnection.d.ts +31 -0
  6. package/dist/core/BaseSSEConnection.js +91 -0
  7. package/dist/core/BatchModelLoader.d.ts +27 -0
  8. package/dist/core/BatchModelLoader.js +70 -0
  9. package/dist/core/CompoundIndexFetcher.d.ts +46 -0
  10. package/dist/core/CompoundIndexFetcher.js +177 -0
  11. package/dist/core/Database.d.ts +303 -0
  12. package/dist/core/Database.js +837 -0
  13. package/dist/core/LazyCollection.d.ts +168 -0
  14. package/dist/core/LazyCollection.js +403 -0
  15. package/dist/core/LazyOwnedCollection.d.ts +35 -0
  16. package/dist/core/LazyOwnedCollection.js +66 -0
  17. package/dist/core/MemoryAdapter.d.ts +67 -0
  18. package/dist/core/MemoryAdapter.js +243 -0
  19. package/dist/core/ModelRegistry.d.ts +64 -0
  20. package/dist/core/ModelRegistry.js +217 -0
  21. package/dist/core/ModelStream.d.ts +33 -0
  22. package/dist/core/ModelStream.js +68 -0
  23. package/dist/core/ObjectPool.d.ts +113 -0
  24. package/dist/core/ObjectPool.js +339 -0
  25. package/dist/core/Store.d.ts +40 -0
  26. package/dist/core/Store.js +73 -0
  27. package/dist/core/StoreManager.d.ts +839 -0
  28. package/dist/core/StoreManager.js +2034 -0
  29. package/dist/core/SyncConnection.d.ts +105 -0
  30. package/dist/core/SyncConnection.js +348 -0
  31. package/dist/core/Transaction.d.ts +114 -0
  32. package/dist/core/Transaction.js +147 -0
  33. package/dist/core/TransactionQueue.d.ts +110 -0
  34. package/dist/core/TransactionQueue.js +601 -0
  35. package/dist/core/decorators.d.ts +66 -0
  36. package/dist/core/decorators.js +278 -0
  37. package/dist/core/hash.d.ts +6 -0
  38. package/dist/core/hash.js +12 -0
  39. package/dist/core/index.d.ts +16 -0
  40. package/dist/core/index.js +18 -0
  41. package/dist/core/internal.d.ts +27 -0
  42. package/dist/core/internal.js +25 -0
  43. package/dist/core/observability.d.ts +21 -0
  44. package/dist/core/observability.js +66 -0
  45. package/dist/core/refAccessors.d.ts +43 -0
  46. package/dist/core/refAccessors.js +80 -0
  47. package/dist/core/serializers.d.ts +2 -0
  48. package/dist/core/serializers.js +2 -0
  49. package/dist/core/types.d.ts +320 -0
  50. package/dist/core/types.js +84 -0
  51. package/dist/react/index.d.ts +82 -0
  52. package/dist/react/index.js +373 -0
  53. package/dist/schema/builders.d.ts +29 -0
  54. package/dist/schema/builders.js +81 -0
  55. package/dist/schema/compile.d.ts +28 -0
  56. package/dist/schema/compile.js +334 -0
  57. package/dist/schema/createStore.d.ts +235 -0
  58. package/dist/schema/createStore.js +264 -0
  59. package/dist/schema/extend.d.ts +46 -0
  60. package/dist/schema/extend.js +6 -0
  61. package/dist/schema/index.d.ts +13 -0
  62. package/dist/schema/index.js +8 -0
  63. package/dist/schema/infer.d.ts +102 -0
  64. package/dist/schema/infer.js +1 -0
  65. package/dist/schema/types.d.ts +76 -0
  66. package/dist/schema/types.js +1 -0
  67. package/dist/schema/zod.d.ts +90 -0
  68. package/dist/schema/zod.js +101 -0
  69. package/package.json +99 -0
@@ -0,0 +1,334 @@
1
+ import { BaseModel } from "../core/BaseModel";
2
+ import { hashString } from "../core/hash";
3
+ import { ModelRegistry } from "../core/ModelRegistry";
4
+ import { defineObservableProperty } from "../core/observability";
5
+ import { installCollectionAccessor, installReferenceAccessor, } from "../core/refAccessors";
6
+ import { PropertyType } from "../core/types";
7
+ /**
8
+ * Compile a `SchemaDef` produced by `defineSchema(...)` into the existing
9
+ * `ModelRegistry`. Each schema entity becomes a synthetic `BaseModel`
10
+ * subclass, registered under its PascalCased key (or `entity({ name })`
11
+ * override). After this returns, `ModelRegistry`, `StoreManager`, and the
12
+ * sync runtime see schema-defined models exactly the way they see
13
+ * decorator-defined ones.
14
+ *
15
+ * The function is pure with respect to the input `schema` object, but
16
+ * registers globally as a side effect — same contract as `@ClientModel`.
17
+ * Validation runs before any registry mutation; on failure the registry
18
+ * is untouched.
19
+ *
20
+ * The four passes below have an ordering dependency: ctors must be created
21
+ * before their fields can carry resolved `referenceTo` registry names; fields
22
+ * must exist before `registerLink` can `updateProperty` the FK; and the
23
+ * per-entity hash must run last so it captures every link side-effect.
24
+ */
25
+ export function compileSchema(schema) {
26
+ validateSchema(schema);
27
+ const nameByKey = resolveNames(schema);
28
+ const ctorByKey = new Map();
29
+ const externalKeys = collectExternalKeys(schema);
30
+ for (const [key, entityDef] of Object.entries(schema.entities)) {
31
+ if (externalKeys.has(key)) {
32
+ continue;
33
+ }
34
+ const name = nameByKey.get(key);
35
+ const ctor = createSyntheticClass(name, entityDef);
36
+ ctorByKey.set(key, ctor);
37
+ const meta = ModelRegistry.registerModel(name, ctor);
38
+ meta.loadStrategy = entityDef.loadStrategy;
39
+ meta.usedForPartialIndexes = entityDef.usedForPartialIndexes ?? false;
40
+ if (entityDef.version != null) {
41
+ meta.schemaVersion = entityDef.version;
42
+ }
43
+ }
44
+ for (const [key, entityDef] of Object.entries(schema.entities)) {
45
+ if (externalKeys.has(key)) {
46
+ continue;
47
+ }
48
+ const name = nameByKey.get(key);
49
+ const ctor = ctorByKey.get(key);
50
+ for (const [fieldName, builder] of Object.entries(entityDef.fields)) {
51
+ registerField(ctor, name, fieldName, builder, nameByKey);
52
+ }
53
+ }
54
+ for (const linkDef of Object.values(schema.links)) {
55
+ registerLink(linkDef, externalKeys, ctorByKey, nameByKey);
56
+ }
57
+ for (const [key, entityDef] of Object.entries(schema.entities)) {
58
+ if (externalKeys.has(key) || entityDef.version != null) {
59
+ continue;
60
+ }
61
+ const name = nameByKey.get(key);
62
+ const meta = ModelRegistry.getModelMeta(name);
63
+ meta.schemaVersion = hashEntityMeta(meta);
64
+ }
65
+ return {
66
+ modelNames: [...nameByKey.values()],
67
+ nameByKey,
68
+ schemaHash: ModelRegistry.schemaHash,
69
+ };
70
+ }
71
+ /**
72
+ * Entity keys that would collide with `store.<top-level>` methods (`store.batch`,
73
+ * future additions). Validation rejects these up front so a schema can't
74
+ * silently shadow the typed surface.
75
+ */
76
+ const RESERVED_DB_KEYS = new Set([
77
+ "batch",
78
+ "undo",
79
+ "redo",
80
+ "undoDepth",
81
+ "redoDepth",
82
+ "runUndoable",
83
+ ]);
84
+ function validateSchema(schema) {
85
+ const errors = [];
86
+ const entityKeys = new Set(Object.keys(schema.entities));
87
+ const seenRegistryNames = new Set();
88
+ for (const [key, entityDef] of Object.entries(schema.entities)) {
89
+ if (RESERVED_DB_KEYS.has(key)) {
90
+ errors.push(`entity key "${key}" collides with the reserved top-level \`store.${key}\`. ` +
91
+ `Rename the entity (e.g. "${key}Entry") or override its registry name.`);
92
+ }
93
+ if (entityDef.external === true && entityDef.name == null) {
94
+ errors.push(`entity "${key}": external: true requires an explicit name so the ` +
95
+ `compiler can resolve cross-references against the existing registry entry.`);
96
+ }
97
+ const name = entityDef.name ?? pascalCase(key);
98
+ if (entityDef.external === true && ModelRegistry.getModelMeta(name) == null) {
99
+ errors.push(`entity "${key}": external model "${name}" is not registered in ` +
100
+ `ModelRegistry. Import/run its @ClientModel definition before compiling the schema.`);
101
+ }
102
+ if (seenRegistryNames.has(name)) {
103
+ errors.push(`Two entities compile to the same registry name "${name}". ` +
104
+ `Override one with \`entity({ name: "..." })\`.`);
105
+ }
106
+ seenRegistryNames.add(name);
107
+ let idCount = 0;
108
+ for (const builder of Object.values(entityDef.fields)) {
109
+ if (builder.meta.kind === "id") {
110
+ idCount++;
111
+ }
112
+ }
113
+ if (idCount > 1) {
114
+ errors.push(`Entity "${key}" declares more than one s.id() field.`);
115
+ }
116
+ }
117
+ const fkBacklinkCount = new Map();
118
+ const relationNamesByEntity = new Map();
119
+ for (const [linkKey, linkDef] of Object.entries(schema.links)) {
120
+ const fromKey = linkDef.from.entity;
121
+ const fieldKey = linkDef.from.field;
122
+ const toKey = linkDef.to.entity;
123
+ if (!entityKeys.has(fromKey)) {
124
+ errors.push(`link "${linkKey}": from.entity "${fromKey}" is not a declared entity. ` +
125
+ `Valid entities: ${[...entityKeys].join(", ")}.`);
126
+ continue;
127
+ }
128
+ if (!entityKeys.has(toKey)) {
129
+ errors.push(`link "${linkKey}": to.entity "${toKey}" is not a declared entity. ` +
130
+ `Valid entities: ${[...entityKeys].join(", ")}.`);
131
+ continue;
132
+ }
133
+ const fromEntity = schema.entities[fromKey];
134
+ const fkBuilder = fromEntity.fields[fieldKey];
135
+ if (fkBuilder == null) {
136
+ errors.push(`link "${linkKey}": field "${fieldKey}" does not exist on entity "${fromKey}".`);
137
+ continue;
138
+ }
139
+ if (fkBuilder.meta.kind !== "refId") {
140
+ errors.push(`link "${linkKey}": field "${fromKey}.${fieldKey}" is ${fkBuilder.meta.kind}; ` +
141
+ `link FKs must be declared with s.refId(...).`);
142
+ continue;
143
+ }
144
+ if (fkBuilder.meta.refTarget !== toKey) {
145
+ errors.push(`link "${linkKey}": s.refId target is "${fkBuilder.meta.refTarget}" ` +
146
+ `but link.to.entity is "${toKey}". They must match.`);
147
+ continue;
148
+ }
149
+ const fkBacklinkKey = `${fromKey}.${fieldKey}`;
150
+ fkBacklinkCount.set(fkBacklinkKey, (fkBacklinkCount.get(fkBacklinkKey) ?? 0) + 1);
151
+ if (fromEntity.fields[linkDef.from.as] != null) {
152
+ errors.push(`link "${linkKey}": from.as "${linkDef.from.as}" collides with a ` +
153
+ `field already declared on entity "${fromKey}".`);
154
+ }
155
+ const toEntity = schema.entities[toKey];
156
+ if (toEntity.fields[linkDef.to.many] != null) {
157
+ errors.push(`link "${linkKey}": to.many "${linkDef.to.many}" collides with a ` +
158
+ `field already declared on entity "${toKey}".`);
159
+ }
160
+ addRelationName(relationNamesByEntity, fromKey, linkDef.from.as, errors);
161
+ addRelationName(relationNamesByEntity, toKey, linkDef.to.many, errors);
162
+ }
163
+ for (const [key, count] of fkBacklinkCount) {
164
+ if (count > 1) {
165
+ errors.push(`FK "${key}" is referenced by ${count} links — each refId field can ` +
166
+ `back at most one link.`);
167
+ }
168
+ }
169
+ if (errors.length > 0) {
170
+ throw new Error(`Schema validation failed:\n - ${errors.join("\n - ")}`);
171
+ }
172
+ }
173
+ function addRelationName(relationNamesByEntity, entityKey, name, errors) {
174
+ let names = relationNamesByEntity.get(entityKey);
175
+ if (names == null) {
176
+ names = new Set();
177
+ relationNamesByEntity.set(entityKey, names);
178
+ }
179
+ if (names.has(name)) {
180
+ errors.push(`entity "${entityKey}": relation property "${name}" is declared by ` +
181
+ `more than one link.`);
182
+ }
183
+ else {
184
+ names.add(name);
185
+ }
186
+ }
187
+ function collectExternalKeys(schema) {
188
+ const out = new Set();
189
+ for (const [key, entityDef] of Object.entries(schema.entities)) {
190
+ if (entityDef.external === true) {
191
+ out.add(key);
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+ function resolveNames(schema) {
197
+ const out = new Map();
198
+ for (const [key, entityDef] of Object.entries(schema.entities)) {
199
+ out.set(key, entityDef.name ?? pascalCase(key));
200
+ }
201
+ return out;
202
+ }
203
+ function pascalCase(input) {
204
+ if (input.length === 0) {
205
+ return input;
206
+ }
207
+ return input[0].toUpperCase() + input.slice(1);
208
+ }
209
+ function createSyntheticClass(name, entityDef) {
210
+ const defaults = collectDefaults(entityDef);
211
+ const ctor = defaults.length === 0
212
+ ? class extends BaseModel {
213
+ }
214
+ : class extends BaseModel {
215
+ constructor() {
216
+ super();
217
+ for (const [key, value] of defaults) {
218
+ this[key] = value;
219
+ }
220
+ }
221
+ };
222
+ Object.defineProperty(ctor, "name", { value: name });
223
+ ctor._modelName = name;
224
+ return ctor;
225
+ }
226
+ function collectDefaults(entityDef) {
227
+ const out = [];
228
+ for (const [fieldName, builder] of Object.entries(entityDef.fields)) {
229
+ if (builder.meta.kind === "id") {
230
+ continue;
231
+ }
232
+ if ("default" in builder.meta && builder.meta.default !== undefined) {
233
+ out.push([fieldName, builder.meta.default]);
234
+ }
235
+ }
236
+ return out;
237
+ }
238
+ function registerField(ctor, modelName, fieldName, builder, nameByKey) {
239
+ const meta = builder.meta;
240
+ if (meta.kind === "id") {
241
+ return;
242
+ }
243
+ const propMeta = {
244
+ name: fieldName,
245
+ type: meta.kind === "refId"
246
+ ? PropertyType.Reference
247
+ : meta.ephemeral
248
+ ? PropertyType.EphemeralProperty
249
+ : PropertyType.Property,
250
+ };
251
+ if (meta.indexed) {
252
+ propMeta.indexed = true;
253
+ }
254
+ if (meta.nullable) {
255
+ propMeta.nullable = true;
256
+ }
257
+ if (meta.serializer != null) {
258
+ propMeta.serializer = meta.serializer;
259
+ }
260
+ if (meta.deserializer != null) {
261
+ propMeta.deserializer = meta.deserializer;
262
+ }
263
+ if (meta.kind === "refId" && meta.refTarget != null) {
264
+ propMeta.referenceTo =
265
+ nameByKey.get(meta.refTarget) ?? pascalCase(meta.refTarget);
266
+ }
267
+ ModelRegistry.registerProperty(modelName, propMeta);
268
+ defineObservableProperty(ctor.prototype, fieldName);
269
+ }
270
+ function registerLink(linkDef, externalKeys, ctorByKey, nameByKey) {
271
+ const fromExternal = externalKeys.has(linkDef.from.entity);
272
+ const toExternal = externalKeys.has(linkDef.to.entity);
273
+ const fromName = nameByKey.get(linkDef.from.entity);
274
+ const toName = nameByKey.get(linkDef.to.entity);
275
+ const fromCtor = ctorByKey.get(linkDef.from.entity);
276
+ const toCtor = ctorByKey.get(linkDef.to.entity);
277
+ const fkField = linkDef.from.field;
278
+ const asField = linkDef.from.as;
279
+ const manyField = linkDef.to.many;
280
+ // From-side updates only apply when the source entity is owned by this
281
+ // schema; for external sources we leave the FK property untouched on the
282
+ // foreign class to avoid clobbering decorator-defined metadata.
283
+ if (!fromExternal && fromCtor != null) {
284
+ const refUpdates = { lazy: true };
285
+ if (linkDef.onDelete != null) {
286
+ refUpdates.onDelete = linkDef.onDelete;
287
+ }
288
+ ModelRegistry.updateProperty(fromName, fkField, refUpdates);
289
+ ModelRegistry.registerProperty(fromName, {
290
+ name: asField,
291
+ type: PropertyType.ReferenceModel,
292
+ referenceTo: toName,
293
+ idField: fkField,
294
+ });
295
+ installReferenceAccessor(fromCtor.prototype, asField, fkField, toName);
296
+ }
297
+ // To-side reverse-collection only when the target entity is owned by this
298
+ // schema. Schema → decorator links don't pollute the decorator's prototype.
299
+ if (!toExternal && toCtor != null) {
300
+ ModelRegistry.registerProperty(toName, {
301
+ name: manyField,
302
+ type: PropertyType.ReferenceCollection,
303
+ referenceTo: fromName,
304
+ lazy: linkDef.to.lazy ?? true,
305
+ inverseOf: fkField,
306
+ });
307
+ installCollectionAccessor(toCtor.prototype, manyField);
308
+ }
309
+ }
310
+ function hashEntityMeta(meta) {
311
+ const props = [...meta.properties.values()]
312
+ .sort((a, b) => a.name.localeCompare(b.name))
313
+ .map((p) => [
314
+ p.name,
315
+ p.type,
316
+ `referenceTo=${p.referenceTo ?? ""}`,
317
+ `inverseOf=${p.inverseOf ?? ""}`,
318
+ `idField=${p.idField ?? ""}`,
319
+ `idsField=${p.idsField ?? ""}`,
320
+ `onDelete=${p.onDelete ?? ""}`,
321
+ `lazy=${p.lazy === true}`,
322
+ `nullable=${p.nullable === true}`,
323
+ `indexed=${p.indexed === true}`,
324
+ `serializer=${p.serializer != null}`,
325
+ `deserializer=${p.deserializer != null}`,
326
+ ].join(";"))
327
+ .join(",");
328
+ return hashString([
329
+ meta.name,
330
+ `loadStrategy=${meta.loadStrategy}`,
331
+ `usedForPartialIndexes=${meta.usedForPartialIndexes}`,
332
+ `props=[${props}]`,
333
+ ].join("|"));
334
+ }
@@ -0,0 +1,235 @@
1
+ import type { StoreManager } from "../core/StoreManager";
2
+ import type { UndoResult } from "../core/TransactionQueue";
3
+ import type { ExtensionDescriptor, MergedExtensionMembers } from "./extend";
4
+ import type { EntityKey, IndexedFieldKeys, InferCreateInput, InferEntity, InferUpdateInput } from "./infer";
5
+ import type { SchemaDef } from "./types";
6
+ /**
7
+ * Curated subset of `BaseModel` lifecycle methods we expose on records so
8
+ * imperative "mutate fields then commit" workflows have a typed path. Keeps
9
+ * the rest of `BaseModel`'s internals (`hydrate`, `serialize`, `assign`,
10
+ * `__mobx`, …) hidden so the public surface stays schema-driven.
11
+ */
12
+ export interface RecordCommitInterface {
13
+ /** Flush pending field changes to the transaction queue. */
14
+ save(): void;
15
+ /** True iff there is at least one pending change since the last save. */
16
+ readonly hasUnsavedChanges: boolean;
17
+ /** Drop pending changes and reset to the last-saved values. */
18
+ discardUnsavedChanges(): void;
19
+ /**
20
+ * MobX-tracked subscription. The selector reads any reactive field or
21
+ * derivation on this record; `cb` fires whenever its result changes.
22
+ * Returns an unsubscribe function.
23
+ *
24
+ * record.watch(r => r.title, (next, prev) => …)
25
+ *
26
+ * Inside React, prefer `useRecord` / `useRelation` — they wire the same
27
+ * thing through `useSyncExternalStore`. This is the imperative path.
28
+ */
29
+ watch<T>(selector: (record: this) => T, cb: (next: T, prev: T) => void): () => void;
30
+ }
31
+ export type RecordWithExtensions<S extends SchemaDef, K extends EntityKey<S>, Exts extends readonly ExtensionDescriptor<S>[]> = InferEntity<S, K> & MergedExtensionMembers<S, K, Exts> & RecordCommitInterface;
32
+ export interface EntityNamespace<S extends SchemaDef, K extends EntityKey<S>, Exts extends readonly ExtensionDescriptor<S>[]> {
33
+ /**
34
+ * Resolve a single record. Pool-first under the hood, so a hit costs only
35
+ * a microtask; a miss falls back to IDB and (if configured) the on-demand
36
+ * fetcher.
37
+ */
38
+ get(id: string): Promise<RecordWithExtensions<S, K, Exts> | null>;
39
+ /** Resolve many records by id. Pool-first per id; missing ones are loaded together. */
40
+ getByIds(ids: readonly string[]): Promise<ReadonlyArray<RecordWithExtensions<S, K, Exts>>>;
41
+ /**
42
+ * Resolve every record matching `value` on a declared `.indexed()` field.
43
+ * The `key` is constrained at the type level to fields actually marked
44
+ * indexed in the schema.
45
+ *
46
+ * `value` is `string` because IDB indexes are string-typed; values from
47
+ * non-string indexed fields (numbers, dates, refIds) need to be stringified
48
+ * the same way the runtime serializes them. Future versions may type the
49
+ * value against the field's TS type once StoreManager.getOrLoadCollection
50
+ * accepts non-string values.
51
+ */
52
+ getByIndex(key: IndexedFieldKeys<S, K>, value: string): Promise<ReadonlyArray<RecordWithExtensions<S, K, Exts>>>;
53
+ /**
54
+ * Resolve every record matching any of `values` on a declared `.indexed()`
55
+ * field. Fans out one `getByIndex` call per value in parallel; the pool
56
+ * dedupes if records appear in multiple buckets. Records are returned in
57
+ * input-`values` order, with duplicates collapsed to first occurrence.
58
+ *
59
+ * If your backend supports compound index queries, opt in via
60
+ * `serverSupportsCompoundIndexKeys: true` + `onDemandIndexBatchFetcher` to
61
+ * collapse the fan-out into one server round-trip when the values share a
62
+ * parent FK. See `agent-docs/04-lazy-loading.md`.
63
+ */
64
+ getByIndexValues(key: IndexedFieldKeys<S, K>, values: readonly string[]): Promise<ReadonlyArray<RecordWithExtensions<S, K, Exts>>>;
65
+ /**
66
+ * Resolve every record of this entity. Hydrates from IDB on first call,
67
+ * relies on partial-index coverage and SSE deltas on subsequent calls.
68
+ */
69
+ getAll(): Promise<ReadonlyArray<RecordWithExtensions<S, K, Exts>>>;
70
+ /**
71
+ * Sync pool snapshot for a single record. Returns `undefined` if the
72
+ * record isn't currently hydrated — `undefined` means "not in this
73
+ * microtask's pool," **not** "doesn't exist." (Mirrors
74
+ * `objectPool.getById`; distinct from `get`, which resolves `null` only
75
+ * after a fetch confirms absence.) Use `get(id)` to fetch, or `has(id)`
76
+ * for a boolean membership check.
77
+ */
78
+ peek(id: string): RecordWithExtensions<S, K, Exts> | undefined;
79
+ /** Sync: is this record currently hydrated in the pool? Pairs with `peek`
80
+ * for code that only needs presence, not the record. */
81
+ has(id: string): boolean;
82
+ /** Sync pool snapshot of every record currently hydrated for this entity. */
83
+ peekAll(): ReadonlyArray<RecordWithExtensions<S, K, Exts>>;
84
+ /**
85
+ * Sync pool filter: every pooled record where `record[key] === value`.
86
+ * `key` is constrained to fields actually marked `.indexed()` in the
87
+ * schema, mirroring `getByIndex` — querying non-indexed fields here is
88
+ * usually a sign you wanted `getByIndex` (which can fall back to IDB).
89
+ */
90
+ peekByIndex(key: IndexedFieldKeys<S, K>, value: string): ReadonlyArray<RecordWithExtensions<S, K, Exts>>;
91
+ /**
92
+ * Create a record and commit it at the current boundary. Returns the live
93
+ * record (already in the pool, transaction enqueued). For a record you
94
+ * want to build up before committing — e.g. a "create on submit, abandon
95
+ * on cancel" form — use `draft(input)` instead.
96
+ */
97
+ create(input: InferCreateInput<S, K>): RecordWithExtensions<S, K, Exts>;
98
+ /**
99
+ * Apply a partial field update and commit it at the current boundary.
100
+ * Returns the record so callers can chain. Throws if no record with `id`
101
+ * is in the pool — to patch a lazy-loaded record, `await get(id)` (or use
102
+ * `draft(id)`) first.
103
+ */
104
+ patch(id: string, fields: InferUpdateInput<S, K>): RecordWithExtensions<S, K, Exts>;
105
+ /**
106
+ * Open a staged editing buffer. Nothing is committed until the returned
107
+ * record's `save()` runs (or an enclosing `batch`/`atomic` flushes it);
108
+ * `discardUnsavedChanges()` rolls back.
109
+ *
110
+ * - `draft(id)` — resolves the existing record (pool → IDB → on-demand,
111
+ * same as `get`) and hands it back in staging mode. Async. Rejects if
112
+ * no record with `id` exists.
113
+ * - `draft(input?)` — a brand-new uncommitted record with its `id` minted
114
+ * up front (so relations can point at it). Sync. Abandoning it without
115
+ * `save()` leaves nothing behind.
116
+ */
117
+ draft(id: string): Promise<RecordWithExtensions<S, K, Exts>>;
118
+ draft(input?: Partial<InferCreateInput<S, K>>): RecordWithExtensions<S, K, Exts>;
119
+ /** Delete the record with full cascade / restrict semantics. Commits at
120
+ * the current boundary. */
121
+ delete(id: string): void;
122
+ /** Soft-delete (archive) the record with full cascade / restrict
123
+ * semantics. Commits at the current boundary. */
124
+ archive(id: string): void;
125
+ /**
126
+ * Hydrate records straight into the pool — no transactions enqueued, no
127
+ * IDB writes. Re-seeding an existing id refreshes that instance in place.
128
+ * For tests and stories, not production.
129
+ */
130
+ seed(records: ReadonlyArray<Partial<InferCreateInput<S, K>>>): ReadonlyArray<RecordWithExtensions<S, K, Exts>>;
131
+ /** Force a network re-fetch of the listed ids. */
132
+ refresh(ids: readonly string[]): Promise<ReadonlyArray<RecordWithExtensions<S, K, Exts>>>;
133
+ /** Force a network re-fetch of every record of this entity. */
134
+ refreshAll(): Promise<void>;
135
+ /**
136
+ * Force a network re-fetch of every record matching `value` on a declared
137
+ * `.indexed()` field. Evicts the partial-index coverage cache first so the
138
+ * next load is guaranteed to hit the server.
139
+ */
140
+ refreshByIndex(key: IndexedFieldKeys<S, K>, value: string): Promise<ReadonlyArray<RecordWithExtensions<S, K, Exts>>>;
141
+ /**
142
+ * Subscribe to pool-level changes for this entity. The callback fires
143
+ * payload-less — re-read with `peekAll` / `peekByIndex` / `peek` inside
144
+ * the handler. Returns an unsubscribe function.
145
+ *
146
+ * Inside React, prefer `useRecords` — it wires the same primitive through
147
+ * `useSyncExternalStore`. This is the imperative path for headless code.
148
+ */
149
+ watchAll(cb: () => void): () => void;
150
+ /**
151
+ * Subscribe to pool-level changes filtered by `record[key] === value`.
152
+ * The pool runs the predicate at write-time and only invokes `cb` when a
153
+ * matching record was added or removed. Cheaper than `watchAll` followed
154
+ * by an in-handler `peekByIndex` filter.
155
+ *
156
+ * Caveat: this fires on **set-membership changes**, not field
157
+ * reassignments. A record moving between buckets via a setter
158
+ * (`record[key] = newValue`) goes through MobX boxes, not pool notify —
159
+ * pair with `record.watch(r => r[key], cb)` if you need that case too.
160
+ */
161
+ watchByIndex(key: IndexedFieldKeys<S, K>, value: string, cb: () => void): () => void;
162
+ }
163
+ /**
164
+ * Top-level `store` methods that aren't entity namespaces. Kept on a sibling
165
+ * intersection so `EntityStore<S>` stays "one entry per entity key" — the schema
166
+ * compiler reserves these names so an entity can't shadow them.
167
+ *
168
+ * For React, prefer `useUndoRedo()` from `zerodrift/react` — it
169
+ * subscribes to the transaction queue so `canUndo` / `canRedo` are
170
+ * reactive. These methods are the imperative path for non-React
171
+ * consumers (CLI tools, headless agents, tests).
172
+ */
173
+ export interface StoreApi {
174
+ /**
175
+ * Run `fn` inside a transaction batch. Every `store.<entity>.create / update /
176
+ * delete` call inside shares a single `batchId`, ships in one HTTP POST,
177
+ * and reverses as one unit on undo. Returns the `batchId`.
178
+ *
179
+ * Accepts both sync and async functions — `endBatch` always fires after
180
+ * the function (or its returned Promise) completes, even on throw.
181
+ *
182
+ * The async overload is declared first so an `async () => {}` literal
183
+ * picks it; a sync `() => {}` returns `void` which can't satisfy
184
+ * `Promise<void>`, so it falls through to the sync overload.
185
+ */
186
+ batch(fn: () => Promise<void>): Promise<string>;
187
+ batch(fn: () => void): string;
188
+ /**
189
+ * Stage optimistic edits with all-or-nothing local commit semantics.
190
+ * Every model touched inside `fn` is `save()`d on success (in one batch
191
+ * → one undo entry) or `discardUnsavedChanges()`d on throw. See
192
+ * `StoreManager.atomic` for the full contract (SSE rebasing during
193
+ * await, runUndoable side effects, no nesting).
194
+ */
195
+ atomic<T>(fn: () => Promise<T>): Promise<T>;
196
+ atomic<T>(fn: () => T): T;
197
+ /** Pop and revert the top of the undo stack. */
198
+ undo(): Promise<UndoResult | null>;
199
+ /** Re-apply the top of the redo stack. */
200
+ redo(): Promise<UndoResult | null>;
201
+ /** Number of entries currently on the undo stack. */
202
+ readonly undoDepth: number;
203
+ /** Number of entries currently on the redo stack. */
204
+ readonly redoDepth: number;
205
+ /**
206
+ * Run a remote side-effect that returns a `changeLogId`, recording it on
207
+ * the undo stack so the next `store.undo()` invokes the
208
+ * `undoableActions.undo` handler with that id. `fn` may return either the
209
+ * `changeLogId` directly or any object carrying one. Inside an open
210
+ * `store.batch(...)`, the action joins the batch.
211
+ */
212
+ runUndoable<T extends string | {
213
+ changeLogId: string;
214
+ }>(fn: () => Promise<T> | T, opts?: {
215
+ actionType?: string;
216
+ metadata?: Record<string, unknown>;
217
+ }): Promise<T>;
218
+ }
219
+ export type EntityStore<S extends SchemaDef, Exts extends readonly ExtensionDescriptor<S>[] = readonly []> = {
220
+ [K in EntityKey<S>]: EntityNamespace<S, K, Exts>;
221
+ } & StoreApi;
222
+ /**
223
+ * Project a `SchemaDef` over a live `StoreManager`. The runtime values are
224
+ * `BaseModel` instances that structurally satisfy the inferred record type;
225
+ * the proxy-based public surface described in the RFC lands later.
226
+ */
227
+ export declare function createStore<S extends SchemaDef, const Exts extends readonly ExtensionDescriptor<S>[] = readonly []>(opts: {
228
+ schema: S;
229
+ storeManager: StoreManager<any>;
230
+ extensions?: Exts;
231
+ }): EntityStore<S, Exts>;
232
+ /** @internal Symbol carrying the ModelRegistry name on each namespace. */
233
+ export declare const REGISTRY_NAME: unique symbol;
234
+ /** @internal Read the registry name a namespace was built for. */
235
+ export declare function entityNamespaceRegistryName(ns: EntityNamespace<SchemaDef, string, readonly ExtensionDescriptor<SchemaDef>[]>): string;