@abloatai/ablo 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecordingTransaction — wraps a base `Transaction` and captures inverse ops
|
|
3
|
+
* for the undo system. Each write is observed BEFORE it runs (to snapshot
|
|
4
|
+
* pre-state) and AFTER (to capture the forward op for redo).
|
|
5
|
+
*
|
|
6
|
+
* The wrapped mutator sees the exact same `Transaction<S>` shape; recording
|
|
7
|
+
* is invisible. When the mutator returns, the caller reads `getEntry()` and
|
|
8
|
+
* pushes it into the active `UndoScope`.
|
|
9
|
+
*
|
|
10
|
+
* Why snapshots live here (not in the UndoScope):
|
|
11
|
+
* - Update inverse requires `prev` field values — must be captured before
|
|
12
|
+
* the write lands in the pool.
|
|
13
|
+
* - Delete inverse requires the full model data — same reason.
|
|
14
|
+
* - Create inverse is simpler (delete by id) but the id must be known
|
|
15
|
+
* post-creation (schema generates UUIDs if caller omitted one).
|
|
16
|
+
*/
|
|
17
|
+
import { createTransaction } from './Transaction.js';
|
|
18
|
+
/**
|
|
19
|
+
* Build a transaction that records inverses + forwards as it runs.
|
|
20
|
+
* Consumers use this only when they want the invocation to be undoable;
|
|
21
|
+
* read-only or side-effect-only mutators should use `createTransaction`
|
|
22
|
+
* directly to avoid the bookkeeping overhead.
|
|
23
|
+
*/
|
|
24
|
+
export function createRecordingTransaction(schema, store, organizationId) {
|
|
25
|
+
const inverses = [];
|
|
26
|
+
const forwards = [];
|
|
27
|
+
const inner = createTransaction(schema, store, organizationId);
|
|
28
|
+
// Wrap mutations with a Proxy that intercepts each model key's
|
|
29
|
+
// methods. We keep `inner.read` as-is — reads don't need recording.
|
|
30
|
+
const mutateProxy = new Proxy({}, {
|
|
31
|
+
get(_target, prop) {
|
|
32
|
+
if (typeof prop !== 'string')
|
|
33
|
+
return undefined;
|
|
34
|
+
const innerMutate = inner.mutations[prop];
|
|
35
|
+
if (!innerMutate)
|
|
36
|
+
return innerMutate;
|
|
37
|
+
return wrapMutateForKey(prop, innerMutate, store, inverses, forwards);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
tx: { mutations: mutateProxy, read: inner.read },
|
|
42
|
+
getEntry: (label) => {
|
|
43
|
+
if (inverses.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
// Undo applies inverses in REVERSE order of how the forward writes ran.
|
|
46
|
+
// Redo applies forwards in the ORIGINAL order.
|
|
47
|
+
return { label, inverses: [...inverses].reverse(), forwards: [...forwards] };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// ── Per-key wrapper ────────────────────────────────────────────────────────
|
|
52
|
+
function wrapMutateForKey(modelKey, mutate, store, inverses, forwards) {
|
|
53
|
+
const snapshot = (id) => {
|
|
54
|
+
const model = store.pool.get(id);
|
|
55
|
+
if (!model)
|
|
56
|
+
return null;
|
|
57
|
+
// Model.toJSON produces a plain object suitable for re-create. We need
|
|
58
|
+
// ALL fields when generating a delete→create inverse, so toJSON's
|
|
59
|
+
// wider shape is exactly right.
|
|
60
|
+
return model.toJSON();
|
|
61
|
+
};
|
|
62
|
+
const snapshotFields = (id, fieldNames) => {
|
|
63
|
+
const model = store.pool.get(id);
|
|
64
|
+
if (!model)
|
|
65
|
+
return null;
|
|
66
|
+
const out = {};
|
|
67
|
+
// `modifiedProperties` is populated by M1's `observe()` listener the
|
|
68
|
+
// moment the caller mutates an observable field directly. Thanks to
|
|
69
|
+
// `Model.propertyChanged`'s first-old-wins policy, `.old` holds the TRUE
|
|
70
|
+
// pre-session baseline even after many in-place mutations (e.g. a drag
|
|
71
|
+
// frame loop). That makes it the authoritative source for the undo
|
|
72
|
+
// inverse when the caller pre-mutates before invoking the mutator.
|
|
73
|
+
//
|
|
74
|
+
// Fallback chain for models/fields that weren't pre-mutated (so no
|
|
75
|
+
// `modifiedProperties` entry exists yet): `getOriginalSnapshot()`
|
|
76
|
+
// (populated on load/`markAsPersisted`/sync-ack), then the live
|
|
77
|
+
// observable. The live read is correct only when the caller didn't
|
|
78
|
+
// touch the field first.
|
|
79
|
+
const original = model.getOriginalSnapshot();
|
|
80
|
+
for (const f of fieldNames) {
|
|
81
|
+
if (f === 'id')
|
|
82
|
+
continue;
|
|
83
|
+
const mod = model.modifiedProperties.get(f);
|
|
84
|
+
if (mod) {
|
|
85
|
+
out[f] = mod.old;
|
|
86
|
+
}
|
|
87
|
+
else if (original && f in original) {
|
|
88
|
+
out[f] = original[f];
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
out[f] = Reflect.get(model, f);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
};
|
|
96
|
+
// After a mutator's `base.update` succeeds, drop the `modifiedProperties`
|
|
97
|
+
// entries we snapshotted from. The next mutator call should see THIS
|
|
98
|
+
// update's result as its baseline, not the pre-session old value. The
|
|
99
|
+
// transaction queue already captured its frozen copy synchronously inside
|
|
100
|
+
// `store.save` (via `captureModelChanges`/`extractPreviousData`), so this
|
|
101
|
+
// clear is safe for server rollback.
|
|
102
|
+
const consumeModifiedFields = (id, fieldNames) => {
|
|
103
|
+
const model = store.pool.get(id);
|
|
104
|
+
if (!model)
|
|
105
|
+
return;
|
|
106
|
+
for (const f of fieldNames) {
|
|
107
|
+
if (f === 'id')
|
|
108
|
+
continue;
|
|
109
|
+
model.modifiedProperties.delete(f);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
// Overloaded — single row or array. The recorder dispatches the
|
|
114
|
+
// matching forward/inverse op shape (`create`/`createMany`,
|
|
115
|
+
// `update`/`updateMany`, `delete`/`deleteMany`) so the persisted
|
|
116
|
+
// undo entry is symmetric with what was originally invoked.
|
|
117
|
+
create: (async (data) => {
|
|
118
|
+
if (Array.isArray(data)) {
|
|
119
|
+
const created = await mutate.create(data);
|
|
120
|
+
const withIds = created.map((m, i) => ({
|
|
121
|
+
...data[i],
|
|
122
|
+
id: m.id,
|
|
123
|
+
}));
|
|
124
|
+
const ids = created.map((m) => m.id);
|
|
125
|
+
forwards.push({ kind: 'createMany', modelKey, data: withIds });
|
|
126
|
+
inverses.push({ kind: 'deleteMany', modelKey, ids });
|
|
127
|
+
return created;
|
|
128
|
+
}
|
|
129
|
+
const created = await mutate.create(data);
|
|
130
|
+
const id = created.id;
|
|
131
|
+
forwards.push({
|
|
132
|
+
kind: 'create',
|
|
133
|
+
modelKey,
|
|
134
|
+
data: { ...data, id },
|
|
135
|
+
});
|
|
136
|
+
inverses.push({ kind: 'delete', modelKey, id });
|
|
137
|
+
return created;
|
|
138
|
+
}),
|
|
139
|
+
update: (async (patch) => {
|
|
140
|
+
if (Array.isArray(patch)) {
|
|
141
|
+
// Snapshot all previous values BEFORE applying — later patches
|
|
142
|
+
// in the same list would corrupt the inverse state of earlier
|
|
143
|
+
// ones if we snapshotted lazily.
|
|
144
|
+
const prevPatches = [];
|
|
145
|
+
for (const p of patch) {
|
|
146
|
+
const fields = Object.keys(p).filter((k) => k !== 'id');
|
|
147
|
+
const prev = snapshotFields(p.id, fields);
|
|
148
|
+
if (prev)
|
|
149
|
+
prevPatches.push({ id: p.id, ...prev });
|
|
150
|
+
}
|
|
151
|
+
const updated = await mutate.update(patch);
|
|
152
|
+
const forwardPatches = patch.map((p) => ({ ...p }));
|
|
153
|
+
for (const p of forwardPatches) {
|
|
154
|
+
consumeModifiedFields(p.id, Object.keys(p).filter((k) => k !== 'id'));
|
|
155
|
+
}
|
|
156
|
+
forwards.push({ kind: 'updateMany', modelKey, patches: forwardPatches });
|
|
157
|
+
if (prevPatches.length > 0) {
|
|
158
|
+
inverses.push({ kind: 'updateMany', modelKey, patches: prevPatches });
|
|
159
|
+
}
|
|
160
|
+
return updated;
|
|
161
|
+
}
|
|
162
|
+
const id = patch.id;
|
|
163
|
+
const fields = Object.keys(patch).filter((k) => k !== 'id');
|
|
164
|
+
const prev = snapshotFields(id, fields);
|
|
165
|
+
const updated = await mutate.update(patch);
|
|
166
|
+
const patchCopy = {
|
|
167
|
+
id,
|
|
168
|
+
...patch,
|
|
169
|
+
};
|
|
170
|
+
consumeModifiedFields(id, fields);
|
|
171
|
+
forwards.push({ kind: 'update', modelKey, patch: patchCopy });
|
|
172
|
+
if (prev) {
|
|
173
|
+
inverses.push({ kind: 'update', modelKey, patch: { id, ...prev } });
|
|
174
|
+
}
|
|
175
|
+
return updated;
|
|
176
|
+
}),
|
|
177
|
+
delete: (async (idOrIds) => {
|
|
178
|
+
if (Array.isArray(idOrIds)) {
|
|
179
|
+
const prevs = idOrIds
|
|
180
|
+
.map((id) => snapshot(id))
|
|
181
|
+
.filter((d) => d !== null);
|
|
182
|
+
await mutate.delete(idOrIds);
|
|
183
|
+
forwards.push({ kind: 'deleteMany', modelKey, ids: [...idOrIds] });
|
|
184
|
+
if (prevs.length > 0) {
|
|
185
|
+
inverses.push({ kind: 'createMany', modelKey, data: prevs });
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const prev = snapshot(idOrIds);
|
|
190
|
+
await mutate.delete(idOrIds);
|
|
191
|
+
forwards.push({ kind: 'delete', modelKey, id: idOrIds });
|
|
192
|
+
if (prev) {
|
|
193
|
+
inverses.push({ kind: 'create', modelKey, data: prev });
|
|
194
|
+
}
|
|
195
|
+
}),
|
|
196
|
+
archive: async (id) => {
|
|
197
|
+
await mutate.archive(id);
|
|
198
|
+
forwards.push({
|
|
199
|
+
kind: 'update',
|
|
200
|
+
modelKey,
|
|
201
|
+
patch: { id, archivedAt: new Date() },
|
|
202
|
+
});
|
|
203
|
+
// Inverse of archive is unarchive, modeled here as a "restore" update.
|
|
204
|
+
inverses.push({ kind: 'update', modelKey, patch: { id, archivedAt: null } });
|
|
205
|
+
},
|
|
206
|
+
unarchive: async (id) => {
|
|
207
|
+
await mutate.unarchive(id);
|
|
208
|
+
forwards.push({ kind: 'update', modelKey, patch: { id, archivedAt: null } });
|
|
209
|
+
inverses.push({
|
|
210
|
+
kind: 'update',
|
|
211
|
+
modelKey,
|
|
212
|
+
patch: { id, archivedAt: new Date() },
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction — Zero-style typed transaction object exposed to custom mutators.
|
|
3
|
+
*
|
|
4
|
+
* A mutator function receives `{ tx, args }`. Through `tx.mutations.<modelKey>.*`
|
|
5
|
+
* it performs writes; through `tx.read.<modelKey>.*` it takes imperative
|
|
6
|
+
* snapshots of the ObjectPool.
|
|
7
|
+
*
|
|
8
|
+
* Semantics:
|
|
9
|
+
* - Writes dispatch eagerly via the existing `createMutateActions` / store
|
|
10
|
+
* primitives (no buffering, no rollback). Partial state is possible if
|
|
11
|
+
* a mutator throws midway. Atomic rollback is a follow-up.
|
|
12
|
+
* - Reads are synchronous snapshots via `createReaderActions`. They use the
|
|
13
|
+
* FK index fast path where available (O(1) on registered FK fields).
|
|
14
|
+
*
|
|
15
|
+
* The mutate surface is intentionally one-row-at-a-time
|
|
16
|
+
* (`create`/`update`/`delete`). For batches, mutator authors compose
|
|
17
|
+
* `Promise.all(rows.map((r) => tx.mutations.x.create(r)))` — every push
|
|
18
|
+
* stages in the same synchronous tick, the await happens once, and the
|
|
19
|
+
* microtask coalescer in `TransactionQueue` collapses N pushes into one
|
|
20
|
+
* wire commit. Same shape Zero uses: no `insertMany`, just an array map.
|
|
21
|
+
*/
|
|
22
|
+
import type { Schema } from '../schema/schema.js';
|
|
23
|
+
import type { SyncStoreContract } from '../react/context.js';
|
|
24
|
+
import { type MutateActions } from '../react/useMutate.js';
|
|
25
|
+
import { type ReaderActions, type ReaderFindOptions } from '../react/useReader.js';
|
|
26
|
+
/**
|
|
27
|
+
* The full transaction surface. `tx.mutations.<key>.*` for writes,
|
|
28
|
+
* `tx.read.<key>.*` for imperative reads. Re-exports the base read options
|
|
29
|
+
* type so mutator authors can type `where` payloads without reaching into
|
|
30
|
+
* the React barrel.
|
|
31
|
+
*
|
|
32
|
+
* The name `mutations` (not `mutate`) matches the React hook naming.
|
|
33
|
+
*/
|
|
34
|
+
export interface Transaction<S extends Schema> {
|
|
35
|
+
mutations: {
|
|
36
|
+
[K in keyof S['models'] & string]: MutateActions<S, K>;
|
|
37
|
+
};
|
|
38
|
+
read: {
|
|
39
|
+
[K in keyof S['models'] & string]: ReaderActions<S, K>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export type { ReaderFindOptions };
|
|
43
|
+
/**
|
|
44
|
+
* Build a Transaction for a single mutator invocation. The returned object
|
|
45
|
+
* lazily instantiates per-model actions on first access so we don't pay for
|
|
46
|
+
* models the mutator never touches.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createTransaction<S extends Schema>(schema: S, store: SyncStoreContract, organizationId: string): Transaction<S>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction — Zero-style typed transaction object exposed to custom mutators.
|
|
3
|
+
*
|
|
4
|
+
* A mutator function receives `{ tx, args }`. Through `tx.mutations.<modelKey>.*`
|
|
5
|
+
* it performs writes; through `tx.read.<modelKey>.*` it takes imperative
|
|
6
|
+
* snapshots of the ObjectPool.
|
|
7
|
+
*
|
|
8
|
+
* Semantics:
|
|
9
|
+
* - Writes dispatch eagerly via the existing `createMutateActions` / store
|
|
10
|
+
* primitives (no buffering, no rollback). Partial state is possible if
|
|
11
|
+
* a mutator throws midway. Atomic rollback is a follow-up.
|
|
12
|
+
* - Reads are synchronous snapshots via `createReaderActions`. They use the
|
|
13
|
+
* FK index fast path where available (O(1) on registered FK fields).
|
|
14
|
+
*
|
|
15
|
+
* The mutate surface is intentionally one-row-at-a-time
|
|
16
|
+
* (`create`/`update`/`delete`). For batches, mutator authors compose
|
|
17
|
+
* `Promise.all(rows.map((r) => tx.mutations.x.create(r)))` — every push
|
|
18
|
+
* stages in the same synchronous tick, the await happens once, and the
|
|
19
|
+
* microtask coalescer in `TransactionQueue` collapses N pushes into one
|
|
20
|
+
* wire commit. Same shape Zero uses: no `insertMany`, just an array map.
|
|
21
|
+
*/
|
|
22
|
+
import { createMutateActions } from '../react/useMutate.js';
|
|
23
|
+
import { createReaderActions } from '../react/useReader.js';
|
|
24
|
+
import { AbloValidationError } from '../errors.js';
|
|
25
|
+
/**
|
|
26
|
+
* Build a Transaction for a single mutator invocation. The returned object
|
|
27
|
+
* lazily instantiates per-model actions on first access so we don't pay for
|
|
28
|
+
* models the mutator never touches.
|
|
29
|
+
*/
|
|
30
|
+
export function createTransaction(schema, store, organizationId) {
|
|
31
|
+
const mutateCache = new Map();
|
|
32
|
+
const readCache = new Map();
|
|
33
|
+
const mutations = new Proxy({}, {
|
|
34
|
+
get(_target, prop) {
|
|
35
|
+
if (typeof prop !== 'string')
|
|
36
|
+
return undefined;
|
|
37
|
+
const cached = mutateCache.get(prop);
|
|
38
|
+
if (cached)
|
|
39
|
+
return cached;
|
|
40
|
+
if (!(prop in schema.models)) {
|
|
41
|
+
throw new AbloValidationError(`Transaction.mutations: unknown model key "${prop}". Known keys: ${Object.keys(schema.models).join(', ')}`, { code: 'transaction_mutate_unknown_model' });
|
|
42
|
+
}
|
|
43
|
+
const actions = createMutateActions(schema, prop, store, organizationId);
|
|
44
|
+
mutateCache.set(prop, actions);
|
|
45
|
+
return actions;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const read = new Proxy({}, {
|
|
49
|
+
get(_target, prop) {
|
|
50
|
+
if (typeof prop !== 'string')
|
|
51
|
+
return undefined;
|
|
52
|
+
const cached = readCache.get(prop);
|
|
53
|
+
if (cached)
|
|
54
|
+
return cached;
|
|
55
|
+
if (!(prop in schema.models)) {
|
|
56
|
+
throw new AbloValidationError(`Transaction.read: unknown model key "${prop}". Known keys: ${Object.keys(schema.models).join(', ')}`, { code: 'transaction_read_unknown_model' });
|
|
57
|
+
}
|
|
58
|
+
const actions = createReaderActions(schema, prop, store);
|
|
59
|
+
readCache.set(prop, actions);
|
|
60
|
+
return actions;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return { mutations, read };
|
|
64
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UndoManager — per-scope history of reversible mutations.
|
|
3
|
+
*
|
|
4
|
+
* Each mutator invocation records an ordered list of inverse operations.
|
|
5
|
+
* On `undo()` we pop the last group and apply the inverses as a non-recorded
|
|
6
|
+
* transaction (so the inverse itself doesn't push to the redo stack; we do
|
|
7
|
+
* that explicitly below).
|
|
8
|
+
*
|
|
9
|
+
* Scopes: every consumer (deck editor, spreadsheet, etc.) gets a named scope
|
|
10
|
+
* via `getScope(name)`. Cmd+Z in one surface never affects another.
|
|
11
|
+
*
|
|
12
|
+
* V1 limitations:
|
|
13
|
+
* - No persistence across sessions (in-memory stack).
|
|
14
|
+
* - No collaborative awareness — undoing after a teammate edited the same
|
|
15
|
+
* row produces a "last writer wins" outcome, not a true merge.
|
|
16
|
+
* - Server-side mutation rejection after optimistic apply does NOT
|
|
17
|
+
* automatically invalidate the undo stack. Consumers should `clear()`
|
|
18
|
+
* the scope on sync error if they want strict correctness.
|
|
19
|
+
*/
|
|
20
|
+
import type { Schema } from '../schema/schema.js';
|
|
21
|
+
import type { SyncStoreContract } from '../react/context.js';
|
|
22
|
+
/**
|
|
23
|
+
* A single reversible operation. The runtime captures these during a
|
|
24
|
+
* recorded transaction and replays them (in reverse order) on undo.
|
|
25
|
+
* Model keys and data shapes are stored as strings/records so the manager
|
|
26
|
+
* is schema-agnostic — the transaction it replays through is schema-typed.
|
|
27
|
+
*/
|
|
28
|
+
export type InverseOp = {
|
|
29
|
+
kind: 'create';
|
|
30
|
+
modelKey: string;
|
|
31
|
+
data: Record<string, unknown>;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'update';
|
|
34
|
+
modelKey: string;
|
|
35
|
+
patch: {
|
|
36
|
+
id: string;
|
|
37
|
+
} & Record<string, unknown>;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'delete';
|
|
40
|
+
modelKey: string;
|
|
41
|
+
id: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'createMany';
|
|
44
|
+
modelKey: string;
|
|
45
|
+
data: Record<string, unknown>[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'updateMany';
|
|
48
|
+
modelKey: string;
|
|
49
|
+
patches: Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
} & Record<string, unknown>>;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'deleteMany';
|
|
54
|
+
modelKey: string;
|
|
55
|
+
ids: string[];
|
|
56
|
+
};
|
|
57
|
+
/** One undo entry = one mutator invocation's set of inverses, in reverse order. */
|
|
58
|
+
export interface UndoEntry {
|
|
59
|
+
/** Optional label for diagnostics / UI ("Move layer", "Delete slide", etc). */
|
|
60
|
+
label?: string;
|
|
61
|
+
inverses: InverseOp[];
|
|
62
|
+
/**
|
|
63
|
+
* Paired forward ops, captured at record time so redo can replay them
|
|
64
|
+
* without re-running the user's mutator (which may have non-idempotent
|
|
65
|
+
* side effects like generating new IDs).
|
|
66
|
+
*/
|
|
67
|
+
forwards: InverseOp[];
|
|
68
|
+
}
|
|
69
|
+
export interface UndoScopeOptions {
|
|
70
|
+
/** Max number of undo entries. Older entries drop off the bottom. Default: 100. */
|
|
71
|
+
maxHistory?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A single undo stack for one surface. Access via `UndoManager.getScope(name)`.
|
|
75
|
+
* Consumers call `record(entry)` after each mutator; `undo()` / `redo()` to
|
|
76
|
+
* traverse the stacks.
|
|
77
|
+
*/
|
|
78
|
+
export declare class UndoScope<S extends Schema> {
|
|
79
|
+
private readonly schema;
|
|
80
|
+
private readonly store;
|
|
81
|
+
private readonly organizationId;
|
|
82
|
+
private undoStack;
|
|
83
|
+
private redoStack;
|
|
84
|
+
private readonly maxHistory;
|
|
85
|
+
constructor(schema: S, store: SyncStoreContract, organizationId: string, options?: UndoScopeOptions);
|
|
86
|
+
/** Internal: record a mutator's inverses. Clears the redo stack. */
|
|
87
|
+
record(entry: UndoEntry): void;
|
|
88
|
+
canUndo(): boolean;
|
|
89
|
+
canRedo(): boolean;
|
|
90
|
+
/** Pop the last mutator and apply its inverses. Pushes to redo. */
|
|
91
|
+
undo(): Promise<void>;
|
|
92
|
+
/** Pop the last undone entry and re-apply the forward ops. Pushes to undo. */
|
|
93
|
+
redo(): Promise<void>;
|
|
94
|
+
/** Drop all history. Use after bootstrap / sync group change / sync error. */
|
|
95
|
+
clear(): void;
|
|
96
|
+
/** Introspection — for debug panels / e2e tests. */
|
|
97
|
+
size(): {
|
|
98
|
+
undo: number;
|
|
99
|
+
redo: number;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Central registry of named undo scopes. One per-app instance, created once
|
|
104
|
+
* during engine setup. Mutator invocations find their scope by name.
|
|
105
|
+
*/
|
|
106
|
+
export declare class UndoManager<S extends Schema> {
|
|
107
|
+
private readonly schema;
|
|
108
|
+
private readonly store;
|
|
109
|
+
private readonly organizationId;
|
|
110
|
+
private readonly scopes;
|
|
111
|
+
constructor(schema: S, store: SyncStoreContract, organizationId: string);
|
|
112
|
+
getScope(name: string, options?: UndoScopeOptions): UndoScope<S>;
|
|
113
|
+
clearAll(): void;
|
|
114
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UndoManager — per-scope history of reversible mutations.
|
|
3
|
+
*
|
|
4
|
+
* Each mutator invocation records an ordered list of inverse operations.
|
|
5
|
+
* On `undo()` we pop the last group and apply the inverses as a non-recorded
|
|
6
|
+
* transaction (so the inverse itself doesn't push to the redo stack; we do
|
|
7
|
+
* that explicitly below).
|
|
8
|
+
*
|
|
9
|
+
* Scopes: every consumer (deck editor, spreadsheet, etc.) gets a named scope
|
|
10
|
+
* via `getScope(name)`. Cmd+Z in one surface never affects another.
|
|
11
|
+
*
|
|
12
|
+
* V1 limitations:
|
|
13
|
+
* - No persistence across sessions (in-memory stack).
|
|
14
|
+
* - No collaborative awareness — undoing after a teammate edited the same
|
|
15
|
+
* row produces a "last writer wins" outcome, not a true merge.
|
|
16
|
+
* - Server-side mutation rejection after optimistic apply does NOT
|
|
17
|
+
* automatically invalidate the undo stack. Consumers should `clear()`
|
|
18
|
+
* the scope on sync error if they want strict correctness.
|
|
19
|
+
*/
|
|
20
|
+
import { createTransaction } from './Transaction.js';
|
|
21
|
+
/**
|
|
22
|
+
* A single undo stack for one surface. Access via `UndoManager.getScope(name)`.
|
|
23
|
+
* Consumers call `record(entry)` after each mutator; `undo()` / `redo()` to
|
|
24
|
+
* traverse the stacks.
|
|
25
|
+
*/
|
|
26
|
+
export class UndoScope {
|
|
27
|
+
schema;
|
|
28
|
+
store;
|
|
29
|
+
organizationId;
|
|
30
|
+
undoStack = [];
|
|
31
|
+
redoStack = [];
|
|
32
|
+
maxHistory;
|
|
33
|
+
constructor(schema, store, organizationId, options = {}) {
|
|
34
|
+
this.schema = schema;
|
|
35
|
+
this.store = store;
|
|
36
|
+
this.organizationId = organizationId;
|
|
37
|
+
this.maxHistory = options.maxHistory ?? 100;
|
|
38
|
+
}
|
|
39
|
+
/** Internal: record a mutator's inverses. Clears the redo stack. */
|
|
40
|
+
record(entry) {
|
|
41
|
+
this.undoStack.push(entry);
|
|
42
|
+
if (this.undoStack.length > this.maxHistory)
|
|
43
|
+
this.undoStack.shift();
|
|
44
|
+
this.redoStack = [];
|
|
45
|
+
}
|
|
46
|
+
canUndo() {
|
|
47
|
+
return this.undoStack.length > 0;
|
|
48
|
+
}
|
|
49
|
+
canRedo() {
|
|
50
|
+
return this.redoStack.length > 0;
|
|
51
|
+
}
|
|
52
|
+
/** Pop the last mutator and apply its inverses. Pushes to redo. */
|
|
53
|
+
async undo() {
|
|
54
|
+
const entry = this.undoStack.pop();
|
|
55
|
+
if (!entry)
|
|
56
|
+
return;
|
|
57
|
+
const tx = createTransaction(this.schema, this.store, this.organizationId);
|
|
58
|
+
await applyOps(tx, entry.inverses);
|
|
59
|
+
this.redoStack.push(entry);
|
|
60
|
+
if (this.redoStack.length > this.maxHistory)
|
|
61
|
+
this.redoStack.shift();
|
|
62
|
+
}
|
|
63
|
+
/** Pop the last undone entry and re-apply the forward ops. Pushes to undo. */
|
|
64
|
+
async redo() {
|
|
65
|
+
const entry = this.redoStack.pop();
|
|
66
|
+
if (!entry)
|
|
67
|
+
return;
|
|
68
|
+
const tx = createTransaction(this.schema, this.store, this.organizationId);
|
|
69
|
+
await applyOps(tx, entry.forwards);
|
|
70
|
+
this.undoStack.push(entry);
|
|
71
|
+
if (this.undoStack.length > this.maxHistory)
|
|
72
|
+
this.undoStack.shift();
|
|
73
|
+
}
|
|
74
|
+
/** Drop all history. Use after bootstrap / sync group change / sync error. */
|
|
75
|
+
clear() {
|
|
76
|
+
this.undoStack = [];
|
|
77
|
+
this.redoStack = [];
|
|
78
|
+
}
|
|
79
|
+
/** Introspection — for debug panels / e2e tests. */
|
|
80
|
+
size() {
|
|
81
|
+
return { undo: this.undoStack.length, redo: this.redoStack.length };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ── Manager ────────────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Central registry of named undo scopes. One per-app instance, created once
|
|
87
|
+
* during engine setup. Mutator invocations find their scope by name.
|
|
88
|
+
*/
|
|
89
|
+
export class UndoManager {
|
|
90
|
+
schema;
|
|
91
|
+
store;
|
|
92
|
+
organizationId;
|
|
93
|
+
scopes = new Map();
|
|
94
|
+
constructor(schema, store, organizationId) {
|
|
95
|
+
this.schema = schema;
|
|
96
|
+
this.store = store;
|
|
97
|
+
this.organizationId = organizationId;
|
|
98
|
+
}
|
|
99
|
+
getScope(name, options) {
|
|
100
|
+
let scope = this.scopes.get(name);
|
|
101
|
+
if (!scope) {
|
|
102
|
+
scope = new UndoScope(this.schema, this.store, this.organizationId, options);
|
|
103
|
+
this.scopes.set(name, scope);
|
|
104
|
+
}
|
|
105
|
+
return scope;
|
|
106
|
+
}
|
|
107
|
+
clearAll() {
|
|
108
|
+
for (const scope of this.scopes.values())
|
|
109
|
+
scope.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ── Internal helpers ───────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Replay a list of InverseOps through a Transaction. Used by both undo
|
|
115
|
+
* (replaying captured inverses) and redo (replaying the captured forwards).
|
|
116
|
+
* Every op is awaited sequentially to preserve ordering guarantees.
|
|
117
|
+
*/
|
|
118
|
+
async function applyOps(tx, ops) {
|
|
119
|
+
const mutateAny = tx.mutations;
|
|
120
|
+
for (const op of ops) {
|
|
121
|
+
const m = mutateAny[op.modelKey];
|
|
122
|
+
switch (op.kind) {
|
|
123
|
+
case 'create':
|
|
124
|
+
await m.create(op.data);
|
|
125
|
+
break;
|
|
126
|
+
case 'update':
|
|
127
|
+
await m.update(op.patch);
|
|
128
|
+
break;
|
|
129
|
+
case 'delete':
|
|
130
|
+
await m.delete(op.id);
|
|
131
|
+
break;
|
|
132
|
+
case 'createMany':
|
|
133
|
+
await m.create(op.data);
|
|
134
|
+
break;
|
|
135
|
+
case 'updateMany':
|
|
136
|
+
await m.update(op.patches);
|
|
137
|
+
break;
|
|
138
|
+
case 'deleteMany':
|
|
139
|
+
await m.delete(op.ids);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineMutators — Zero-style custom mutator declaration.
|
|
3
|
+
*
|
|
4
|
+
* Consumers declare a tree of named mutators grouped by model key. Each
|
|
5
|
+
* mutator is a plain async function that receives `{ tx, args }`. The body
|
|
6
|
+
* composes any number of `tx.mutate.*` / `tx.read.*` calls to implement a
|
|
7
|
+
* named operation (e.g. `slides.createWithLayers`).
|
|
8
|
+
*
|
|
9
|
+
* This file is pure type scaffolding + a pass-through factory. The runtime
|
|
10
|
+
* dispatcher lives in `./Transaction` (the `tx` object) and
|
|
11
|
+
* `../react/useMutators` (the React-side invoker builder). Keeping those
|
|
12
|
+
* concerns separate makes the types trivially inferable at the call site:
|
|
13
|
+
* `defineMutators(schema, { ... })` returns the literal object the consumer
|
|
14
|
+
* wrote, so `typeof mutators` carries every mutator's exact `args`/result
|
|
15
|
+
* signature into `useMutators`.
|
|
16
|
+
*/
|
|
17
|
+
import type { Schema } from '../schema/schema.js';
|
|
18
|
+
import type { Transaction } from './Transaction.js';
|
|
19
|
+
/**
|
|
20
|
+
* Signature of a single custom mutator. The host injects `tx`; the consumer
|
|
21
|
+
* controls `args` (whatever shape they want) and the resolved return value.
|
|
22
|
+
*
|
|
23
|
+
* We bound `TArgs`/`TResult` with `unknown` rather than `any` so consumers
|
|
24
|
+
* opt into the inference they need — the `MutatorDefs` record relaxes to
|
|
25
|
+
* `unknown` to let heterogeneous mutator trees unify without `any`.
|
|
26
|
+
*/
|
|
27
|
+
export type MutatorFn<S extends Schema, TArgs, TResult = void> = (options: {
|
|
28
|
+
tx: Transaction<S>;
|
|
29
|
+
args: TArgs;
|
|
30
|
+
}) => Promise<TResult>;
|
|
31
|
+
/**
|
|
32
|
+
* The shape `defineMutators` accepts: an optional record per model key whose
|
|
33
|
+
* values are named mutator functions.
|
|
34
|
+
*
|
|
35
|
+
* We intentionally use `unknown` in the bounds rather than `any` to preserve
|
|
36
|
+
* type-safety at the public API boundary. When a consumer writes their
|
|
37
|
+
* mutators inline, TypeScript infers the concrete `TArgs`/`TResult` for each
|
|
38
|
+
* function — the `unknown` here is just a ceiling, not what the consumer
|
|
39
|
+
* ends up seeing.
|
|
40
|
+
*/
|
|
41
|
+
export type MutatorDefs<S extends Schema> = {
|
|
42
|
+
[K in keyof S['models']]?: {
|
|
43
|
+
[mutatorName: string]: MutatorFn<S, never, unknown>;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Identity function that forwards the mutators object while constraining its
|
|
48
|
+
* shape against the schema. The `S` generic pins model keys; the `M` generic
|
|
49
|
+
* is `const`-inferred so each mutator's literal signature survives.
|
|
50
|
+
*
|
|
51
|
+
* Pattern mirrors Zero's own `defineMutators` / `createBuilder` — there is
|
|
52
|
+
* no runtime work to do here, it's purely a location for type inference to
|
|
53
|
+
* anchor.
|
|
54
|
+
*/
|
|
55
|
+
export declare function defineMutators<S extends Schema, const M extends MutatorDefs<S>>(_schema: S, mutators: M): M;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineMutators — Zero-style custom mutator declaration.
|
|
3
|
+
*
|
|
4
|
+
* Consumers declare a tree of named mutators grouped by model key. Each
|
|
5
|
+
* mutator is a plain async function that receives `{ tx, args }`. The body
|
|
6
|
+
* composes any number of `tx.mutate.*` / `tx.read.*` calls to implement a
|
|
7
|
+
* named operation (e.g. `slides.createWithLayers`).
|
|
8
|
+
*
|
|
9
|
+
* This file is pure type scaffolding + a pass-through factory. The runtime
|
|
10
|
+
* dispatcher lives in `./Transaction` (the `tx` object) and
|
|
11
|
+
* `../react/useMutators` (the React-side invoker builder). Keeping those
|
|
12
|
+
* concerns separate makes the types trivially inferable at the call site:
|
|
13
|
+
* `defineMutators(schema, { ... })` returns the literal object the consumer
|
|
14
|
+
* wrote, so `typeof mutators` carries every mutator's exact `args`/result
|
|
15
|
+
* signature into `useMutators`.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Identity function that forwards the mutators object while constraining its
|
|
19
|
+
* shape against the schema. The `S` generic pins model keys; the `M` generic
|
|
20
|
+
* is `const`-inferred so each mutator's literal signature survives.
|
|
21
|
+
*
|
|
22
|
+
* Pattern mirrors Zero's own `defineMutators` / `createBuilder` — there is
|
|
23
|
+
* no runtime work to do here, it's purely a location for type inference to
|
|
24
|
+
* anchor.
|
|
25
|
+
*/
|
|
26
|
+
export function defineMutators(_schema, mutators) {
|
|
27
|
+
return mutators;
|
|
28
|
+
}
|