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,373 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * React integration for the Sync Engine.
4
+ *
5
+ * Hooks subscribe to ObjectPool change notifications via useSyncExternalStore,
6
+ * so a delta packet that adds, updates, or removes a model automatically
7
+ * re-renders any component reading it through `useRecord` / `useRecords` /
8
+ * `useRecordsByIndex` (or a relation via `useRelation`).
9
+ */
10
+ import { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef, useSyncExternalStore, useLayoutEffect, } from "react";
11
+ import { StoreManager } from "../core/StoreManager";
12
+ import { BootstrapPhase } from "../core/types";
13
+ import { BackRef } from "../core/LazyCollection";
14
+ import { readFk } from "../core/ObjectPool";
15
+ // `<any>` keeps the hooks free of TContext — none of them touch it.
16
+ const SyncContext = createContext(null);
17
+ // ---------------------------------------------------------------------------
18
+ // Provider
19
+ // ---------------------------------------------------------------------------
20
+ export function SyncProvider({ config, context, children, fallback, }) {
21
+ const [status, setStatus] = useState({
22
+ phase: BootstrapPhase.Idle,
23
+ });
24
+ const smRef = useRef(null);
25
+ const cfgRef = useRef(config);
26
+ cfgRef.current = config;
27
+ // Detect bfcache restores. When a tab is duplicated (or the user navigates
28
+ // back/forward) the browser may restore the page from its back/forward cache
29
+ // (bfcache). In that case the JS heap is frozen and thawed — React effects do
30
+ // NOT re-run, so the StoreManager never bootstraps and the fallback stays
31
+ // visible forever. Reloading on persisted pageshow breaks out of that state.
32
+ useEffect(() => {
33
+ const handlePageShow = (e) => {
34
+ if (e.persisted) {
35
+ window.location.reload();
36
+ }
37
+ };
38
+ window.addEventListener("pageshow", handlePageShow);
39
+ return () => window.removeEventListener("pageshow", handlePageShow);
40
+ }, []);
41
+ // Latest context, sampled at render time. Captured into a ref so the
42
+ // construction effect can seed the StoreManager without re-running when
43
+ // the context changes (the dedicated effect below pushes updates).
44
+ const contextRef = useRef(context);
45
+ contextRef.current = context;
46
+ useEffect(() => {
47
+ let active = true;
48
+ const sm = new StoreManager({
49
+ ...cfgRef.current,
50
+ hooks: {
51
+ ...cfgRef.current.hooks,
52
+ onPhaseChange: (phase, detail) => {
53
+ cfgRef.current.hooks?.onPhaseChange?.(phase, detail);
54
+ if (active) {
55
+ setStatus({ phase, detail });
56
+ }
57
+ },
58
+ },
59
+ });
60
+ if (contextRef.current !== undefined) {
61
+ sm.setContext(contextRef.current);
62
+ }
63
+ smRef.current = sm;
64
+ sm.bootstrap().catch((err) => {
65
+ if (active) {
66
+ setStatus({ phase: BootstrapPhase.Error, error: String(err) });
67
+ }
68
+ });
69
+ return () => {
70
+ active = false;
71
+ sm.teardown();
72
+ smRef.current = null;
73
+ };
74
+ }, [cfgRef.current.workspaceId]);
75
+ // Push context updates synchronously so an event handler dispatched in the
76
+ // same commit as a context change sees the fresh value when minting ids.
77
+ useLayoutEffect(() => {
78
+ if (smRef.current != null && context !== undefined) {
79
+ smRef.current.setContext(context);
80
+ }
81
+ }, [context]);
82
+ if (smRef.current == null) {
83
+ return fallback != null ? _jsx(_Fragment, { children: fallback }) : null;
84
+ }
85
+ if (status.phase !== BootstrapPhase.Ready &&
86
+ status.phase !== BootstrapPhase.Error &&
87
+ fallback != null) {
88
+ return _jsx(_Fragment, { children: fallback });
89
+ }
90
+ return (_jsx(SyncContext.Provider, { value: { sm: smRef.current, status }, children: children }));
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Core hook
94
+ // ---------------------------------------------------------------------------
95
+ export function useSyncEngine() {
96
+ const ctx = useContext(SyncContext);
97
+ if (ctx == null) {
98
+ throw new Error("useSyncEngine() must be inside <SyncProvider>");
99
+ }
100
+ return ctx;
101
+ }
102
+ export function useBootstrapStatus() {
103
+ return useSyncEngine().status;
104
+ }
105
+ /** Subscribe to a model type's pool changes and read a snapshot synchronously.
106
+ *
107
+ * `getSnapshot` is intentionally NOT stabilized — useSyncExternalStore calls
108
+ * it during render and compares the returned value, not the function identity.
109
+ * Stabilizing via useStableCallback would defer ref-updates to useLayoutEffect
110
+ * and silently return stale values on the render where its inputs change. */
111
+ function usePoolSnapshot(modelName, getSnapshot) {
112
+ const { sm } = useSyncEngine();
113
+ const subscribe = useCallback((onStoreChange) => sm.objectPool.subscribe(modelName, onStoreChange), [sm, modelName]);
114
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // useLoader — internal helper carrying the loading/error/reload + race-guard
118
+ // shape shared by every pool-aware hook below. Auto-fires on mount and on
119
+ // `triggerKey` change when `shouldAutoFire` returns true; `reload()` always
120
+ // fires regardless of the gate.
121
+ // ---------------------------------------------------------------------------
122
+ function useLoader(load, enabled, triggerKey, shouldAutoFire) {
123
+ const [isLoading, setIsLoading] = useState(false);
124
+ const [error, setError] = useState(null);
125
+ const gen = useRef(0);
126
+ const stableLoad = useStableCallback(load);
127
+ const stableShouldAutoFire = useStableCallback(shouldAutoFire);
128
+ const reload = useCallback(async () => {
129
+ if (!enabled) {
130
+ return;
131
+ }
132
+ const g = ++gen.current;
133
+ setIsLoading(true);
134
+ setError(null);
135
+ try {
136
+ await stableLoad();
137
+ if (g === gen.current) {
138
+ setIsLoading(false);
139
+ }
140
+ }
141
+ catch (e) {
142
+ if (g === gen.current) {
143
+ setError(e);
144
+ setIsLoading(false);
145
+ }
146
+ }
147
+ }, [enabled, triggerKey, stableLoad]);
148
+ useEffect(() => {
149
+ if (enabled && stableShouldAutoFire()) {
150
+ void reload();
151
+ }
152
+ }, [reload, enabled, triggerKey, stableShouldAutoFire]);
153
+ return { isLoading, error, reload };
154
+ }
155
+ /** `isLoaded` for the pool-keyed hooks: ready, not loading, no error — true
156
+ * from frame zero on a pool hit (the loader's auto-fire is gated). */
157
+ const settled = (ready, isLoading, error) => ready && !isLoading && error == null;
158
+ // Model-name-keyed implementations. The public hooks resolve a handle
159
+ // (schema namespace or model class) to a registry name and delegate here,
160
+ // so the pool-subscription / loader machinery lives in exactly one place.
161
+ /** Reactive single model by id. Pool-first sync read; async backfill on miss. */
162
+ function useRecordByName(modelName, id) {
163
+ const { sm, status } = useSyncEngine();
164
+ const pool = sm.objectPool;
165
+ const ready = status.phase === BootstrapPhase.Ready;
166
+ const item = usePoolSnapshot(modelName, () => id != null ? (pool.getById(modelName, id) ?? null) : null);
167
+ const { isLoading, error, reload } = useLoader(() => sm.getOrLoadById(modelName, id), ready && id != null, `${modelName}:${id ?? ""}`,
168
+ // Skip the load when the pool already has the entry — eager models
169
+ // render with isLoading: false from frame zero.
170
+ () => id != null && pool.getById(modelName, id) == null);
171
+ return {
172
+ data: ready ? item : null,
173
+ isLoading,
174
+ isLoaded: settled(ready, isLoading, error),
175
+ error,
176
+ reload,
177
+ };
178
+ }
179
+ /** Reactive list of models of a type, optionally filtered to a specific id
180
+ * set. Without `ids`: every instance in the pool. With `ids`: just those, in
181
+ * the order given, with async backfill for any missing from the pool. The
182
+ * ids array is compared by content so inline literals don't cause re-fetches. */
183
+ function useRecordsByName(modelName, ids) {
184
+ const { sm, status } = useSyncEngine();
185
+ const pool = sm.objectPool;
186
+ const ready = status.phase === BootstrapPhase.Ready;
187
+ const idsKey = ids?.join(",") ?? "";
188
+ const all = usePoolSnapshot(modelName, () => pool.getAll(modelName));
189
+ const items = useMemo(() => {
190
+ if (ids == null) {
191
+ return all;
192
+ }
193
+ const byId = new Map(all.map((m) => [m.id, m]));
194
+ return ids
195
+ .map((id) => byId.get(id))
196
+ .filter((m) => m != null);
197
+ }, [all, idsKey]);
198
+ const { isLoading, error, reload } = useLoader(() => sm.getOrLoadByIds(modelName, ids ?? []), ready && ids != null && ids.length > 0, `${modelName}:${idsKey}`, () => ids != null && ids.some((id) => pool.getById(modelName, id) == null));
199
+ return {
200
+ data: ready ? items : [],
201
+ isLoading,
202
+ isLoaded: settled(ready, isLoading, error),
203
+ error,
204
+ reload,
205
+ };
206
+ }
207
+ /** Reactive list of models matching one OR many values on a foreign-key
208
+ * index. A single string and a `string[]` take the same path (the single
209
+ * value is a one-element set), so semantics are identical. Coverage is
210
+ * tracked per `(name, indexKey, value)` so re-renders don't re-fetch
211
+ * already-covered buckets; values are compared by content so inline literals
212
+ * don't trigger re-fetches.
213
+ *
214
+ * For one-round-trip multi-value fetches, configure
215
+ * `onDemandIndexBatchFetcher` + `serverSupportsCompoundIndexKeys: true` —
216
+ * see `agent-docs/04-lazy-loading.md`. */
217
+ function useRecordsByIndexName(modelName, indexKey, value) {
218
+ const { sm, status } = useSyncEngine();
219
+ const ready = status.phase === BootstrapPhase.Ready;
220
+ const values = value == null
221
+ ? []
222
+ : (Array.isArray(value) ? value : [value]).filter((v) => v != null && v !== "");
223
+ const valuesKey = values.join(",");
224
+ const hasValues = values.length > 0;
225
+ const all = usePoolSnapshot(modelName, () => sm.objectPool.getAll(modelName));
226
+ const items = useMemo(() => {
227
+ if (!hasValues) {
228
+ return [];
229
+ }
230
+ const set = new Set(values);
231
+ return all.filter((m) => {
232
+ const v = readFk(m, indexKey);
233
+ return v != null && set.has(v);
234
+ });
235
+ // valuesKey: content equality; array identity is unstable for inline literals.
236
+ }, [all, indexKey, valuesKey, hasValues]);
237
+ const { isLoading, error, reload } = useLoader(async () => {
238
+ await Promise.all(values.map((v) => sm.getOrLoadCollection(modelName, indexKey, v)));
239
+ }, ready && hasValues, `${modelName}:${indexKey}:${valuesKey}`, () => hasValues &&
240
+ values.some((v) => !sm.isCollectionLoaded(modelName, indexKey, v)));
241
+ return {
242
+ data: ready ? items : [],
243
+ isLoading,
244
+ isLoaded: settled(ready, isLoading, error),
245
+ error,
246
+ reload,
247
+ };
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // Batch and undo/redo
251
+ // ---------------------------------------------------------------------------
252
+ /** Returns `store.batch` — the sync overload yields the `batchId` string,
253
+ * the async overload a `Promise<string>`. */
254
+ export function useBatch() {
255
+ const { sm } = useSyncEngine();
256
+ return useCallback(((fn) => sm.batch(fn)), [sm]);
257
+ }
258
+ export function useUndoRedo() {
259
+ const { sm } = useSyncEngine();
260
+ const snapshotRef = useRef({ undoDepth: 0, redoDepth: 0 });
261
+ const subscribe = useCallback((onStoreChange) => sm.transactionQueue.subscribe(onStoreChange), [sm]);
262
+ const getSnapshot = useCallback(() => {
263
+ const undoDepth = sm.transactionQueue.undoDepth;
264
+ const redoDepth = sm.transactionQueue.redoDepth;
265
+ if (snapshotRef.current.undoDepth !== undoDepth ||
266
+ snapshotRef.current.redoDepth !== redoDepth) {
267
+ snapshotRef.current = { undoDepth, redoDepth };
268
+ }
269
+ return snapshotRef.current;
270
+ }, [sm]);
271
+ const { undoDepth, redoDepth } = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
272
+ return {
273
+ undo: useCallback(() => sm.undo(), [sm]),
274
+ redo: useCallback(() => sm.redo(), [sm]),
275
+ canUndo: undoDepth > 0,
276
+ canRedo: redoDepth > 0,
277
+ };
278
+ }
279
+ export function useRelation(relation) {
280
+ const [tick, forceRender] = useState(0);
281
+ const isBackRef = relation instanceof BackRef;
282
+ // Collections expose watch() for invalidation; BackRef does not.
283
+ useEffect(() => {
284
+ if (relation == null || isBackRef) {
285
+ return;
286
+ }
287
+ return relation.watch(() => forceRender((n) => n + 1));
288
+ }, [relation, isBackRef]);
289
+ useEffect(() => {
290
+ if (relation != null && !relation.isLoaded && !relation.isLoading) {
291
+ relation.load().then(() => forceRender((n) => n + 1));
292
+ }
293
+ }, [relation, tick]);
294
+ if (relation == null) {
295
+ // Can't tell collection from back-ref at null; `[]` is the map-safe
296
+ // default. A null back-ref reads `[]` rather than `null` — harmless
297
+ // (consumers use `data?.x`), and the relation object is non-null
298
+ // whenever its holding record exists.
299
+ return {
300
+ data: [],
301
+ isLoading: false,
302
+ isLoaded: false,
303
+ error: null,
304
+ reload: async () => { },
305
+ };
306
+ }
307
+ return {
308
+ data: isBackRef
309
+ ? (relation.value ?? null)
310
+ : (relation.items ?? []),
311
+ isLoading: relation.isLoading ?? false,
312
+ isLoaded: relation.isLoaded ?? false,
313
+ error: relation.error ?? null,
314
+ reload: async () => {
315
+ await (isBackRef
316
+ ? relation.load()
317
+ : relation.reload());
318
+ },
319
+ };
320
+ }
321
+ function useStableCallback(callback) {
322
+ const computedRef = useRef(callback);
323
+ const stableRef = useRef((...args) => {
324
+ return computedRef.current(...args);
325
+ });
326
+ useLayoutEffect(function updateStableCallbackRef() {
327
+ computedRef.current = callback;
328
+ }, [callback]);
329
+ return stableRef.current;
330
+ }
331
+ // ---------------------------------------------------------------------------
332
+ // Public read hooks — keyed by a "handle"
333
+ //
334
+ // A handle is either a schema namespace (`store.issue`) or a decorator
335
+ // model class (`Issue`). Both resolve to a registry name; the record type
336
+ // is inferred from whichever form was passed, so the same four hooks serve
337
+ // both authoring paths with one vocabulary:
338
+ //
339
+ // useRecord(handle, id) → AsyncResource<T | null>
340
+ // useRecords(handle, ids?) → AsyncResource<T[]>
341
+ // useRecordsByIndex(handle, key, v|v[]) → AsyncResource<T[]>
342
+ // useRelation(record.relation) → AsyncResource<T[] | T | null>
343
+ //
344
+ // For namespace handles the index key is constrained to the schema's
345
+ // `.indexed()` fields; for class handles it's `string`.
346
+ // ---------------------------------------------------------------------------
347
+ import { entityNamespaceRegistryName, } from "../schema/createStore";
348
+ function handleRegistryName(handle) {
349
+ if (typeof handle === "function") {
350
+ // Set by @ClientModel (explicit { name } or ctor.name fallback).
351
+ return (handle._modelName ??
352
+ handle.name);
353
+ }
354
+ return entityNamespaceRegistryName(handle);
355
+ }
356
+ // `as unknown as` bridges `BaseModel` (what the internal name-keyed hooks
357
+ // return) and `RecordOf<H>` (the typed view of the same pooled instance).
358
+ // Same object at runtime; neither type is assignable to the other (BaseModel
359
+ // has `__mobx`/`store`; RecordOf has schema fields + extensions). One cast
360
+ // per wrapper, contained.
361
+ /** Reactive single record by id. Pool-first sync read; async backfill on miss. */
362
+ export function useRecord(handle, id) {
363
+ return useRecordByName(handleRegistryName(handle), id);
364
+ }
365
+ /** Reactive list of records, optionally filtered to (and ordered by) `ids`. */
366
+ export function useRecords(handle, ids) {
367
+ return useRecordsByName(handleRegistryName(handle), ids);
368
+ }
369
+ /** Reactive list of records matching one value, or any of several, on a
370
+ * foreign-key index. */
371
+ export function useRecordsByIndex(handle, indexKey, value) {
372
+ return useRecordsByIndexName(handleRegistryName(handle), indexKey, value);
373
+ }
@@ -0,0 +1,29 @@
1
+ import type { AnyFieldBuilder, AnyLinkDef, EntityDef, FieldBuilder, FieldMeta, LinkDef, SchemaDef } from "./types";
2
+ export declare function rebuildFieldBuilder<T, M extends FieldMeta>(meta: M): FieldBuilder<T, M>;
3
+ export declare const fields: {
4
+ id: () => FieldBuilder<string, FieldMeta & {
5
+ kind: "id";
6
+ }>;
7
+ string: () => FieldBuilder<string, FieldMeta & {
8
+ kind: "string";
9
+ }>;
10
+ number: () => FieldBuilder<number, FieldMeta & {
11
+ kind: "number";
12
+ }>;
13
+ boolean: () => FieldBuilder<boolean, FieldMeta & {
14
+ kind: "boolean";
15
+ }>;
16
+ date: () => FieldBuilder<Date, FieldMeta & {
17
+ kind: "date";
18
+ }>;
19
+ json: <T = unknown>() => FieldBuilder<T, FieldMeta & {
20
+ kind: "json";
21
+ }>;
22
+ refId: <Target extends string>(target: Target) => FieldBuilder<string, FieldMeta & {
23
+ kind: "refId";
24
+ refTarget: Target;
25
+ }>;
26
+ };
27
+ export declare function entity<const F extends Record<string, AnyFieldBuilder>>(def: EntityDef<F>): EntityDef<F>;
28
+ export declare function link<const FromEntity extends string, const FromField extends string, const As extends string, const ToEntity extends string, const Many extends string>(def: LinkDef<FromEntity, FromField, As, ToEntity, Many>): LinkDef<FromEntity, FromField, As, ToEntity, Many>;
29
+ export declare function defineSchema<const E extends Record<string, EntityDef>, const L extends Record<string, AnyLinkDef>>(schema: SchemaDef<E, L>): SchemaDef<E, L>;
@@ -0,0 +1,81 @@
1
+ import { dateDeserializer, dateSerializer } from "../core/serializers";
2
+ function makeBuilder(meta) {
3
+ return {
4
+ meta,
5
+ nullable() {
6
+ return makeBuilder({ ...meta, nullable: true });
7
+ },
8
+ indexed() {
9
+ return makeBuilder({
10
+ ...meta,
11
+ indexed: true,
12
+ });
13
+ },
14
+ default(value) {
15
+ return makeBuilder({
16
+ ...meta,
17
+ default: value,
18
+ });
19
+ },
20
+ ephemeral() {
21
+ return makeBuilder({ ...meta, ephemeral: true });
22
+ },
23
+ serialize(fn) {
24
+ return makeBuilder({
25
+ ...meta,
26
+ serializer: fn,
27
+ });
28
+ },
29
+ deserialize(fn) {
30
+ return makeBuilder({ ...meta, deserializer: fn });
31
+ },
32
+ };
33
+ }
34
+ export function rebuildFieldBuilder(meta) {
35
+ return makeBuilder(meta);
36
+ }
37
+ const baseFlags = {
38
+ nullable: false,
39
+ optional: false,
40
+ indexed: false,
41
+ ephemeral: false,
42
+ };
43
+ function field(kind, extras) {
44
+ return makeBuilder({
45
+ kind,
46
+ ...baseFlags,
47
+ ...extras,
48
+ });
49
+ }
50
+ const id = () => field("id");
51
+ const stringField = () => field("string");
52
+ const numberField = () => field("number");
53
+ const booleanField = () => field("boolean");
54
+ const date = () => field("date", {
55
+ serializer: dateSerializer,
56
+ deserializer: dateDeserializer,
57
+ });
58
+ const json = () => field("json");
59
+ const refId = (target) => makeBuilder({
60
+ kind: "refId",
61
+ refTarget: target,
62
+ ...baseFlags,
63
+ });
64
+ export const fields = {
65
+ id,
66
+ string: stringField,
67
+ number: numberField,
68
+ boolean: booleanField,
69
+ date,
70
+ json,
71
+ refId,
72
+ };
73
+ export function entity(def) {
74
+ return def;
75
+ }
76
+ export function link(def) {
77
+ return def;
78
+ }
79
+ export function defineSchema(schema) {
80
+ return schema;
81
+ }
@@ -0,0 +1,28 @@
1
+ import type { SchemaDef } from "./types";
2
+ export interface CompiledSchema {
3
+ /** Registry names of every entity that was compiled. */
4
+ modelNames: readonly string[];
5
+ /** Map from schema-entity key to registry name. */
6
+ nameByKey: ReadonlyMap<string, string>;
7
+ /** Snapshot of the global registry hash after compilation. */
8
+ schemaHash: string;
9
+ }
10
+ /**
11
+ * Compile a `SchemaDef` produced by `defineSchema(...)` into the existing
12
+ * `ModelRegistry`. Each schema entity becomes a synthetic `BaseModel`
13
+ * subclass, registered under its PascalCased key (or `entity({ name })`
14
+ * override). After this returns, `ModelRegistry`, `StoreManager`, and the
15
+ * sync runtime see schema-defined models exactly the way they see
16
+ * decorator-defined ones.
17
+ *
18
+ * The function is pure with respect to the input `schema` object, but
19
+ * registers globally as a side effect — same contract as `@ClientModel`.
20
+ * Validation runs before any registry mutation; on failure the registry
21
+ * is untouched.
22
+ *
23
+ * The four passes below have an ordering dependency: ctors must be created
24
+ * before their fields can carry resolved `referenceTo` registry names; fields
25
+ * must exist before `registerLink` can `updateProperty` the FK; and the
26
+ * per-entity hash must run last so it captures every link side-effect.
27
+ */
28
+ export declare function compileSchema(schema: SchemaDef): CompiledSchema;