tinybase-mergeable-history 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 INSP LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # tinybase-mergeable-history
2
+
3
+ Persistent, syncable, **CRDT-aware** undo/redo history for [TinyBase](https://tinybase.org).
4
+
5
+ TinyBase's built-in [`Checkpoints`](https://tinybase.org/api/checkpoints/) keep an undo stack in memory: it is lost on reload, scoped to a single client, and unaware of `MergeableStore` merges. `tinybase-mergeable-history` takes the opposite approach — **history is stored as data inside the store itself** (in a `_history` table). Because the history lives in the store, it travels with:
6
+
7
+ - **any Persister** → undo/redo survives reloads
8
+ - **any Synchronizer** → undo/redo is shared across clients
9
+ - **`MergeableStore`** → undo/redo are ordinary mergeable mutations, so reverting an action on one client merges cleanly with concurrent edits on another instead of desyncing peers
10
+ - **author attribution** → every action can be stamped with who made it, recorded as data so the attribution persists and syncs alongside the history
11
+
12
+ ## Comparison
13
+
14
+ | | Built-in `Checkpoints` | `tinybase-mergeable-history` |
15
+ | ----------------------------- | ---------------------- | --------------------------------- |
16
+ | Storage | in-memory | as data in the store (`_history`) |
17
+ | Survives reload | ❌ | ✅ (via any Persister) |
18
+ | Shared across clients | ❌ | ✅ (via any Synchronizer) |
19
+ | MergeableStore / CRDT-aware | ❌ | ✅ |
20
+ | Per-action author attribution | ❌ | ✅ (`getAuthor`) |
21
+ | Linear undo/redo | ✅ | ✅ |
22
+ | Count- and time-based pruning | ❌ | ✅ (`maxActions`, `maxAge`) |
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ pnpm add tinybase-mergeable-history tinybase
28
+ ```
29
+
30
+ `tinybase` is a peer dependency (`^8.0.0`).
31
+
32
+ ## Quick start
33
+
34
+ ```ts
35
+ import { createStore } from 'tinybase';
36
+ import { createHistory } from 'tinybase-mergeable-history';
37
+
38
+ const store = createStore();
39
+ const history = createHistory(store);
40
+
41
+ // Wrap the mutations you want to be undoable in `action()`.
42
+ history.action(() => {
43
+ store.setCell('pets', 'fido', 'sold', true);
44
+ });
45
+
46
+ history.canGoBackward(); // true
47
+ history.goBackward(); // undoes the action
48
+ history.canGoForward(); // true
49
+ history.goForward(); // redoes it
50
+ ```
51
+
52
+ ### With a MergeableStore (multi-client, CRDT)
53
+
54
+ ```ts
55
+ import { createMergeableStore } from 'tinybase';
56
+ import { createHistory } from 'tinybase-mergeable-history';
57
+
58
+ const store = createMergeableStore('client-1');
59
+ const history = createHistory(store);
60
+
61
+ // Attach any persister / synchronizer as usual — the `_history`
62
+ // table travels with the store, so undo state persists and syncs,
63
+ // and undo/redo merge cleanly across clients.
64
+ ```
65
+
66
+ ## Author attribution
67
+
68
+ In a shared, multi-user store it is often useful to know **who** performed each undoable action. Pass a `getAuthor` callback and every recorded action stores its author alongside the delta, in the same `_history` row — so the attribution persists and syncs just like the rest of the history.
69
+
70
+ ```ts
71
+ const history = createHistory(store, {
72
+ getAuthor: () => currentUser.id, // called when an action is recorded
73
+ });
74
+
75
+ const actionId = history.action(() => {
76
+ store.setCell('pets', 'fido', 'sold', true);
77
+ });
78
+
79
+ // Read it back from the history row:
80
+ const author = history.getStore().getCell('_history', actionId!, 'author');
81
+ ```
82
+
83
+ `getAuthor` defaults to `() => ''` (no attribution). The author is captured at the moment the action is recorded, so it reflects whoever made the change — even after it is undone, redone, or synced to another client.
84
+
85
+ ## API
86
+
87
+ ### `createHistory(store, options?): History`
88
+
89
+ | option | default | description |
90
+ | ------------ | ------------ | ---------------------------------------------------------------------------------------- |
91
+ | `tableName` | `'_history'` | table the history is stored in |
92
+ | `maxActions` | `200` | maximum number of actions retained |
93
+ | `maxAge` | `30 days` | maximum age (ms) of retained actions |
94
+ | `getAuthor` | `() => ''` | returns an identifier for who is making the action; stored on the action's `author` cell |
95
+ | `generateId` | built-in | action id generator |
96
+ | `withBypass` | `fn => fn()` | wrap internal writes (e.g. to bypass your listeners) |
97
+
98
+ ### `History`
99
+
100
+ - `action(mutate): Id | undefined` — run `mutate` and record it as one undoable action
101
+ - `goBackward()` / `goForward()` — undo / redo
102
+ - `canGoBackward()` / `canGoForward(): boolean`
103
+ - `getActionIds(): [backward[], current, forward[]]`
104
+ - `setMaxActions(max)` / `setMaxAge(ms)` — tune retention
105
+ - `cleanup()` — prune now
106
+ - `addCleanupListener(listener)` / `delListener(id)` — observe pruning (cascade GC)
107
+ - `getStore(): Store`
108
+ - `destroy()` — remove listeners
109
+
110
+ ### Schematized stores
111
+
112
+ If you use a Zod tables schema, spread in the history table schema so history writes are allowed:
113
+
114
+ ```ts
115
+ import { historyTablesSchema } from 'tinybase-mergeable-history';
116
+
117
+ const tablesSchema = {
118
+ ...historyTablesSchema,
119
+ pets: z.object({ sold: z.boolean() }),
120
+ };
121
+ ```
122
+
123
+ The history table schema includes the `author` cell used for attribution.
124
+
125
+ ## How it works
126
+
127
+ Every `action()` runs inside a TinyBase transaction. A `willFinishTransaction` listener captures the transaction's cell and value changes and writes a single row to the history table holding the forward and reverse deltas, an `undone` flag, and the action's `author`. Undo and redo are themselves ordinary store mutations that flip `undone` and re-apply the relevant delta — which is exactly why they persist, sync, and merge like any other data.
128
+
129
+ ## License
130
+
131
+ MIT © INSP LLC
@@ -0,0 +1,55 @@
1
+ import { Id, Store } from 'tinybase';
2
+ import { z } from 'zod';
3
+
4
+ interface HistoryOptions {
5
+ tableName?: string;
6
+ maxActions?: number;
7
+ maxAge?: number;
8
+ generateId?: () => string;
9
+ getAuthor?: () => string;
10
+ withBypass?: (fn: () => void) => void;
11
+ }
12
+ interface History {
13
+ action(mutate: () => void): Id | undefined;
14
+ goBackward(): this;
15
+ goForward(): this;
16
+ canGoBackward(): boolean;
17
+ canGoForward(): boolean;
18
+ getActionIds(): [Id[], Id | undefined, Id[]];
19
+ setMaxActions(max: number): this;
20
+ setMaxAge(ms: number): this;
21
+ cleanup(): this;
22
+ addCleanupListener(listener: CleanupListener): Id;
23
+ getStore(): Store;
24
+ delListener(listenerId: Id): this;
25
+ destroy(): void;
26
+ }
27
+ type CleanupListener = (history: History, actions: CleanedAction[]) => void;
28
+ interface CleanedAction {
29
+ id: string;
30
+ createdAt: number;
31
+ undone: boolean;
32
+ delta: string;
33
+ deltaValues: string;
34
+ }
35
+
36
+ declare function createHistory(store: Store, options?: HistoryOptions): History;
37
+
38
+ /**
39
+ * Zod schema for the history table.
40
+ * Spread into your store's Zod tables schema to allow history writes.
41
+ *
42
+ * Usage:
43
+ * const myTablesSchema = { ...historyTablesSchema, yourTable: z.object({...}) };
44
+ */
45
+ declare const historyTablesSchema: {
46
+ _history: z.ZodObject<{
47
+ createdAt: z.ZodNumber;
48
+ delta: z.ZodString;
49
+ dv: z.ZodString;
50
+ undone: z.ZodNumber;
51
+ author: z.ZodDefault<z.ZodString>;
52
+ }, z.core.$strip>;
53
+ };
54
+
55
+ export { type CleanedAction, type CleanupListener, type History, type HistoryOptions, createHistory, historyTablesSchema };
@@ -0,0 +1,55 @@
1
+ import { Id, Store } from 'tinybase';
2
+ import { z } from 'zod';
3
+
4
+ interface HistoryOptions {
5
+ tableName?: string;
6
+ maxActions?: number;
7
+ maxAge?: number;
8
+ generateId?: () => string;
9
+ getAuthor?: () => string;
10
+ withBypass?: (fn: () => void) => void;
11
+ }
12
+ interface History {
13
+ action(mutate: () => void): Id | undefined;
14
+ goBackward(): this;
15
+ goForward(): this;
16
+ canGoBackward(): boolean;
17
+ canGoForward(): boolean;
18
+ getActionIds(): [Id[], Id | undefined, Id[]];
19
+ setMaxActions(max: number): this;
20
+ setMaxAge(ms: number): this;
21
+ cleanup(): this;
22
+ addCleanupListener(listener: CleanupListener): Id;
23
+ getStore(): Store;
24
+ delListener(listenerId: Id): this;
25
+ destroy(): void;
26
+ }
27
+ type CleanupListener = (history: History, actions: CleanedAction[]) => void;
28
+ interface CleanedAction {
29
+ id: string;
30
+ createdAt: number;
31
+ undone: boolean;
32
+ delta: string;
33
+ deltaValues: string;
34
+ }
35
+
36
+ declare function createHistory(store: Store, options?: HistoryOptions): History;
37
+
38
+ /**
39
+ * Zod schema for the history table.
40
+ * Spread into your store's Zod tables schema to allow history writes.
41
+ *
42
+ * Usage:
43
+ * const myTablesSchema = { ...historyTablesSchema, yourTable: z.object({...}) };
44
+ */
45
+ declare const historyTablesSchema: {
46
+ _history: z.ZodObject<{
47
+ createdAt: z.ZodNumber;
48
+ delta: z.ZodString;
49
+ dv: z.ZodString;
50
+ undone: z.ZodNumber;
51
+ author: z.ZodDefault<z.ZodString>;
52
+ }, z.core.$strip>;
53
+ };
54
+
55
+ export { type CleanedAction, type CleanupListener, type History, type HistoryOptions, createHistory, historyTablesSchema };
package/build/index.js ADDED
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ // src/delta.ts
6
+ function deserializeDelta(json) {
7
+ return JSON.parse(json);
8
+ }
9
+ function deserializeDeltaValues(json) {
10
+ return JSON.parse(json);
11
+ }
12
+ function applyDelta(store, delta, dv, index) {
13
+ for (const tableId of Object.keys(delta)) {
14
+ for (const rowId of Object.keys(delta[tableId])) {
15
+ for (const cellId of Object.keys(delta[tableId][rowId])) {
16
+ const value = delta[tableId][rowId][cellId][index];
17
+ if (value == null) {
18
+ store.delCell(tableId, rowId, cellId, true);
19
+ } else {
20
+ store.setCell(tableId, rowId, cellId, value);
21
+ }
22
+ }
23
+ }
24
+ }
25
+ for (const valueId of Object.keys(dv)) {
26
+ const value = dv[valueId][index];
27
+ if (value == null) {
28
+ store.delValue(valueId);
29
+ } else {
30
+ store.setValue(valueId, value);
31
+ }
32
+ }
33
+ }
34
+ function applyReverse(store, delta, dv) {
35
+ applyDelta(store, delta, dv, 0);
36
+ }
37
+ function applyForward(store, delta, dv) {
38
+ applyDelta(store, delta, dv, 1);
39
+ }
40
+
41
+ // src/cleanup.ts
42
+ function runCleanup(store, tableName, maxActions, maxAge, history, listeners, withBypass) {
43
+ const now = Date.now();
44
+ const rowIds = store.getRowIds(tableName);
45
+ const actions = rowIds.map((id) => ({
46
+ id,
47
+ createdAt: store.getCell(tableName, id, "createdAt"),
48
+ undone: store.getCell(tableName, id, "undone"),
49
+ delta: store.getCell(tableName, id, "delta"),
50
+ dv: store.getCell(tableName, id, "dv")
51
+ }));
52
+ actions.sort((a, b) => a.createdAt - b.createdAt);
53
+ const toRemoveSet = /* @__PURE__ */ new Set();
54
+ for (const action of actions) {
55
+ if (now - action.createdAt > maxAge) {
56
+ toRemoveSet.add(action.id);
57
+ }
58
+ }
59
+ const remaining = actions.filter((a) => !toRemoveSet.has(a.id));
60
+ if (remaining.length > maxActions) {
61
+ const excess = remaining.length - maxActions;
62
+ for (let i = 0; i < excess; i++) {
63
+ toRemoveSet.add(remaining[i].id);
64
+ }
65
+ }
66
+ if (toRemoveSet.size === 0) return;
67
+ const toRemove = actions.filter((a) => toRemoveSet.has(a.id));
68
+ const cleaned = toRemove.map((a) => ({
69
+ id: a.id,
70
+ createdAt: a.createdAt,
71
+ undone: a.undone === 1,
72
+ delta: a.delta,
73
+ deltaValues: a.dv
74
+ }));
75
+ for (const listener of Array.from(listeners.values())) {
76
+ listener(history, cleaned);
77
+ }
78
+ withBypass(() => {
79
+ store.transaction(() => {
80
+ for (const action of toRemove) {
81
+ store.delRow(tableName, action.id);
82
+ }
83
+ });
84
+ });
85
+ }
86
+
87
+ // src/history.ts
88
+ var History = class {
89
+ constructor(store, tableName, maxActions, maxAge, generateId, getAuthor, bypassFn) {
90
+ this.store = store;
91
+ this.tableName = tableName;
92
+ this.maxActions = maxActions;
93
+ this.maxAge = maxAge;
94
+ this.generateId = generateId;
95
+ this.getAuthor = getAuthor;
96
+ this.bypassFn = bypassFn;
97
+ this.pendingActionRecorded = false;
98
+ this.cleanupListeners = /* @__PURE__ */ new Map();
99
+ this.nextListenerId = 0;
100
+ this.txListenerId = store.addWillFinishTransactionListener(() => {
101
+ if (!this.pendingAction) return;
102
+ const [, , changedCells, , changedValues] = store.getTransactionLog();
103
+ const filteredCells = { ...changedCells };
104
+ delete filteredCells[this.tableName];
105
+ const hasCellChanges = Object.keys(filteredCells).length > 0;
106
+ const hasValueChanges = Object.keys(changedValues).length > 0;
107
+ if (!hasCellChanges && !hasValueChanges) return;
108
+ const id = this.pendingAction;
109
+ this.bypassFn(() => {
110
+ store.setRow(this.tableName, id, {
111
+ createdAt: Date.now(),
112
+ delta: JSON.stringify(filteredCells),
113
+ dv: JSON.stringify(changedValues),
114
+ undone: 0,
115
+ author: this.getAuthor()
116
+ });
117
+ });
118
+ this.pendingActionRecorded = true;
119
+ });
120
+ }
121
+ action(mutate) {
122
+ const id = this.generateId();
123
+ const undoneIds = this.store.getRowIds(this.tableName).filter(
124
+ (rowId) => this.store.getCell(this.tableName, rowId, "undone") === 1
125
+ );
126
+ if (undoneIds.length > 0) {
127
+ this.bypassFn(() => {
128
+ this.store.transaction(() => {
129
+ for (const rowId of undoneIds) {
130
+ this.store.delRow(this.tableName, rowId);
131
+ }
132
+ });
133
+ });
134
+ }
135
+ this.pendingAction = id;
136
+ this.pendingActionRecorded = false;
137
+ try {
138
+ this.store.transaction(() => mutate());
139
+ } catch (err) {
140
+ this.store.finishTransaction();
141
+ this.pendingAction = void 0;
142
+ throw err;
143
+ }
144
+ this.pendingAction = void 0;
145
+ return this.pendingActionRecorded ? id : void 0;
146
+ }
147
+ goBackward() {
148
+ const [, current] = this.getActionIds();
149
+ if (!current) return this;
150
+ const row = this.store.getRow(this.tableName, current);
151
+ const delta = deserializeDelta(row.delta);
152
+ const dv = deserializeDeltaValues(row.dv);
153
+ this.store.transaction(() => {
154
+ applyReverse(this.store, delta, dv);
155
+ this.bypassFn(() => {
156
+ this.store.setCell(this.tableName, current, "undone", 1);
157
+ });
158
+ });
159
+ return this;
160
+ }
161
+ goForward() {
162
+ const [, , forward] = this.getActionIds();
163
+ if (forward.length === 0) return this;
164
+ const actionId = forward[0];
165
+ const row = this.store.getRow(this.tableName, actionId);
166
+ const delta = deserializeDelta(row.delta);
167
+ const dv = deserializeDeltaValues(row.dv);
168
+ this.store.transaction(() => {
169
+ applyForward(this.store, delta, dv);
170
+ this.bypassFn(() => {
171
+ this.store.setCell(this.tableName, actionId, "undone", 0);
172
+ });
173
+ });
174
+ return this;
175
+ }
176
+ canGoBackward() {
177
+ const [, current] = this.getActionIds();
178
+ return current !== void 0;
179
+ }
180
+ canGoForward() {
181
+ const [, , forward] = this.getActionIds();
182
+ return forward.length > 0;
183
+ }
184
+ getActionIds() {
185
+ const rowIds = this.store.getRowIds(this.tableName);
186
+ const sorted = [...rowIds].sort(
187
+ (a, b) => this.store.getCell(this.tableName, a, "createdAt") - this.store.getCell(this.tableName, b, "createdAt")
188
+ );
189
+ let lastActiveIdx = -1;
190
+ for (let i = sorted.length - 1; i >= 0; i--) {
191
+ if (this.store.getCell(this.tableName, sorted[i], "undone") === 0) {
192
+ lastActiveIdx = i;
193
+ break;
194
+ }
195
+ }
196
+ const backward = [];
197
+ const forward = [];
198
+ for (let i = 0; i < sorted.length; i++) {
199
+ const id = sorted[i];
200
+ const undone = this.store.getCell(this.tableName, id, "undone") === 1;
201
+ if (undone && i > lastActiveIdx) {
202
+ forward.push(id);
203
+ } else if (!undone) {
204
+ backward.push(id);
205
+ }
206
+ }
207
+ const current = backward.pop();
208
+ return [backward, current, forward];
209
+ }
210
+ setMaxActions(max) {
211
+ this.maxActions = max;
212
+ return this;
213
+ }
214
+ setMaxAge(ms) {
215
+ this.maxAge = ms;
216
+ return this;
217
+ }
218
+ cleanup() {
219
+ runCleanup(
220
+ this.store,
221
+ this.tableName,
222
+ this.maxActions,
223
+ this.maxAge,
224
+ this,
225
+ this.cleanupListeners,
226
+ this.bypassFn
227
+ );
228
+ return this;
229
+ }
230
+ addCleanupListener(listener) {
231
+ const id = String(this.nextListenerId++);
232
+ this.cleanupListeners.set(id, listener);
233
+ return id;
234
+ }
235
+ getStore() {
236
+ return this.store;
237
+ }
238
+ delListener(listenerId) {
239
+ this.cleanupListeners.delete(listenerId);
240
+ return this;
241
+ }
242
+ destroy() {
243
+ this.store.delListener(this.txListenerId);
244
+ this.cleanupListeners.clear();
245
+ }
246
+ };
247
+
248
+ // src/create.ts
249
+ var THIRTY_DAYS = 30 * 24 * 60 * 60 * 1e3;
250
+ var counter = 0;
251
+ function defaultGenerateId() {
252
+ const ts = Date.now().toString(36);
253
+ const rand = Math.random().toString(36).slice(2, 8);
254
+ return `${ts}-${rand}-${String(++counter).padStart(4, "0")}`;
255
+ }
256
+ function createHistory(store, options) {
257
+ return new History(
258
+ store,
259
+ options?.tableName ?? "_history",
260
+ options?.maxActions ?? 200,
261
+ options?.maxAge ?? THIRTY_DAYS,
262
+ options?.generateId ?? defaultGenerateId,
263
+ options?.getAuthor ?? (() => ""),
264
+ options?.withBypass ?? ((fn) => fn())
265
+ );
266
+ }
267
+ var historyTablesSchema = {
268
+ _history: zod.z.object({
269
+ createdAt: zod.z.number(),
270
+ delta: zod.z.string(),
271
+ dv: zod.z.string(),
272
+ undone: zod.z.number(),
273
+ author: zod.z.string().default("")
274
+ })
275
+ };
276
+
277
+ exports.createHistory = createHistory;
278
+ exports.historyTablesSchema = historyTablesSchema;
279
+ //# sourceMappingURL=index.js.map
280
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/delta.ts","../src/cleanup.ts","../src/history.ts","../src/create.ts","../src/schema.ts"],"names":["z"],"mappings":"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","file":"index.js","sourcesContent":["import type { Store } from 'tinybase';\nimport type { ParsedDelta, ParsedDeltaValues } from './types';\n\nexport function deserializeDelta(json: string): ParsedDelta {\n return JSON.parse(json);\n}\n\nexport function deserializeDeltaValues(json: string): ParsedDeltaValues {\n return JSON.parse(json);\n}\n\nfunction applyDelta(\n store: Store,\n delta: ParsedDelta,\n dv: ParsedDeltaValues,\n index: 0 | 1\n): void {\n for (const tableId of Object.keys(delta)) {\n for (const rowId of Object.keys(delta[tableId])) {\n for (const cellId of Object.keys(delta[tableId][rowId])) {\n const value = delta[tableId][rowId][cellId][index];\n if (value == null) {\n store.delCell(tableId, rowId, cellId, true);\n } else {\n store.setCell(tableId, rowId, cellId, value);\n }\n }\n }\n }\n for (const valueId of Object.keys(dv)) {\n const value = dv[valueId][index];\n if (value == null) {\n store.delValue(valueId);\n } else {\n store.setValue(valueId, value);\n }\n }\n}\n\nexport function applyReverse(\n store: Store,\n delta: ParsedDelta,\n dv: ParsedDeltaValues\n): void {\n applyDelta(store, delta, dv, 0);\n}\n\nexport function applyForward(\n store: Store,\n delta: ParsedDelta,\n dv: ParsedDeltaValues\n): void {\n applyDelta(store, delta, dv, 1);\n}\n","import type { Store } from 'tinybase';\nimport type { History, CleanedAction, CleanupListener } from './types';\n\nexport function runCleanup(\n store: Store,\n tableName: string,\n maxActions: number,\n maxAge: number,\n history: History,\n listeners: Map<string, CleanupListener>,\n withBypass: (fn: () => void) => void\n): void {\n const now = Date.now();\n const rowIds = store.getRowIds(tableName);\n\n const actions = rowIds.map((id) => ({\n id,\n createdAt: store.getCell(tableName, id, 'createdAt') as number,\n undone: store.getCell(tableName, id, 'undone') as number,\n delta: store.getCell(tableName, id, 'delta') as string,\n dv: store.getCell(tableName, id, 'dv') as string,\n }));\n\n actions.sort((a, b) => a.createdAt - b.createdAt);\n\n const toRemoveSet = new Set<string>();\n\n for (const action of actions) {\n if (now - action.createdAt > maxAge) {\n toRemoveSet.add(action.id);\n }\n }\n\n const remaining = actions.filter((a) => !toRemoveSet.has(a.id));\n if (remaining.length > maxActions) {\n const excess = remaining.length - maxActions;\n for (let i = 0; i < excess; i++) {\n toRemoveSet.add(remaining[i].id);\n }\n }\n\n if (toRemoveSet.size === 0) return;\n\n const toRemove = actions.filter((a) => toRemoveSet.has(a.id));\n\n const cleaned: CleanedAction[] = toRemove.map((a) => ({\n id: a.id,\n createdAt: a.createdAt,\n undone: a.undone === 1,\n delta: a.delta,\n deltaValues: a.dv,\n }));\n\n for (const listener of Array.from(listeners.values())) {\n listener(history, cleaned);\n }\n\n withBypass(() => {\n store.transaction(() => {\n for (const action of toRemove) {\n store.delRow(tableName, action.id);\n }\n });\n });\n}\n","import type { Store, Id } from 'tinybase';\nimport type { History as IHistory, CleanupListener } from './types';\nimport {\n deserializeDelta,\n deserializeDeltaValues,\n applyReverse,\n applyForward,\n} from './delta';\nimport { runCleanup } from './cleanup';\n\nexport class History implements IHistory {\n private pendingAction: string | undefined;\n private pendingActionRecorded = false;\n private readonly cleanupListeners = new Map<string, CleanupListener>();\n private nextListenerId = 0;\n private txListenerId: Id;\n\n constructor(\n private readonly store: Store,\n private readonly tableName: string,\n private maxActions: number,\n private maxAge: number,\n private readonly generateId: () => string,\n private readonly getAuthor: () => string,\n private readonly bypassFn: (fn: () => void) => void\n ) {\n this.txListenerId = store.addWillFinishTransactionListener(() => {\n if (!this.pendingAction) return;\n\n const [, , changedCells, , changedValues] = store.getTransactionLog();\n\n const filteredCells = { ...changedCells };\n delete filteredCells[this.tableName];\n\n const hasCellChanges = Object.keys(filteredCells).length > 0;\n const hasValueChanges = Object.keys(changedValues).length > 0;\n\n if (!hasCellChanges && !hasValueChanges) return;\n\n const id = this.pendingAction;\n\n this.bypassFn(() => {\n store.setRow(this.tableName, id, {\n createdAt: Date.now(),\n delta: JSON.stringify(filteredCells),\n dv: JSON.stringify(changedValues),\n undone: 0,\n author: this.getAuthor(),\n });\n });\n\n this.pendingActionRecorded = true;\n });\n }\n\n action(mutate: () => void): Id | undefined {\n const id = this.generateId();\n\n const undoneIds = this.store\n .getRowIds(this.tableName)\n .filter(\n (rowId) => this.store.getCell(this.tableName, rowId, 'undone') === 1\n );\n if (undoneIds.length > 0) {\n this.bypassFn(() => {\n this.store.transaction(() => {\n for (const rowId of undoneIds) {\n this.store.delRow(this.tableName, rowId);\n }\n });\n });\n }\n\n this.pendingAction = id;\n this.pendingActionRecorded = false;\n try {\n this.store.transaction(() => mutate());\n } catch (err) {\n // TinyBase skips finishTransaction on throw, leaving the store\n // stuck. Manually finish so listeners fire and the store recovers.\n this.store.finishTransaction();\n this.pendingAction = undefined;\n throw err;\n }\n this.pendingAction = undefined;\n\n return this.pendingActionRecorded ? id : undefined;\n }\n\n goBackward(): this {\n const [, current] = this.getActionIds();\n if (!current) return this;\n\n const row = this.store.getRow(this.tableName, current);\n const delta = deserializeDelta(row.delta as string);\n const dv = deserializeDeltaValues(row.dv as string);\n\n this.store.transaction(() => {\n applyReverse(this.store, delta, dv);\n this.bypassFn(() => {\n this.store.setCell(this.tableName, current, 'undone', 1);\n });\n });\n\n return this;\n }\n\n goForward(): this {\n const [, , forward] = this.getActionIds();\n if (forward.length === 0) return this;\n\n const actionId = forward[0];\n const row = this.store.getRow(this.tableName, actionId);\n const delta = deserializeDelta(row.delta as string);\n const dv = deserializeDeltaValues(row.dv as string);\n\n this.store.transaction(() => {\n applyForward(this.store, delta, dv);\n this.bypassFn(() => {\n this.store.setCell(this.tableName, actionId, 'undone', 0);\n });\n });\n\n return this;\n }\n\n canGoBackward(): boolean {\n const [, current] = this.getActionIds();\n return current !== undefined;\n }\n\n canGoForward(): boolean {\n const [, , forward] = this.getActionIds();\n return forward.length > 0;\n }\n\n getActionIds(): [Id[], Id | undefined, Id[]] {\n const rowIds = this.store.getRowIds(this.tableName);\n const sorted = [...rowIds].sort(\n (a, b) =>\n (this.store.getCell(this.tableName, a, 'createdAt') as number) -\n (this.store.getCell(this.tableName, b, 'createdAt') as number)\n );\n\n let lastActiveIdx = -1;\n for (let i = sorted.length - 1; i >= 0; i--) {\n if (this.store.getCell(this.tableName, sorted[i], 'undone') === 0) {\n lastActiveIdx = i;\n break;\n }\n }\n\n const backward: Id[] = [];\n const forward: Id[] = [];\n\n for (let i = 0; i < sorted.length; i++) {\n const id = sorted[i];\n const undone = this.store.getCell(this.tableName, id, 'undone') === 1;\n if (undone && i > lastActiveIdx) {\n forward.push(id);\n } else if (!undone) {\n backward.push(id);\n }\n }\n\n const current = backward.pop();\n return [backward, current, forward];\n }\n\n setMaxActions(max: number): this {\n this.maxActions = max;\n return this;\n }\n\n setMaxAge(ms: number): this {\n this.maxAge = ms;\n return this;\n }\n\n cleanup(): this {\n runCleanup(\n this.store,\n this.tableName,\n this.maxActions,\n this.maxAge,\n this,\n this.cleanupListeners,\n this.bypassFn\n );\n return this;\n }\n\n addCleanupListener(listener: CleanupListener): Id {\n const id = String(this.nextListenerId++);\n this.cleanupListeners.set(id, listener);\n return id;\n }\n\n getStore(): Store {\n return this.store;\n }\n\n delListener(listenerId: Id): this {\n this.cleanupListeners.delete(listenerId);\n return this;\n }\n\n destroy(): void {\n this.store.delListener(this.txListenerId);\n this.cleanupListeners.clear();\n }\n}\n","import type { Store } from 'tinybase';\nimport type { History as IHistory, HistoryOptions } from './types';\nimport { History } from './history';\n\nconst THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;\n\nlet counter = 0;\nfunction defaultGenerateId(): string {\n const ts = Date.now().toString(36);\n const rand = Math.random().toString(36).slice(2, 8);\n return `${ts}-${rand}-${String(++counter).padStart(4, '0')}`;\n}\n\nexport function createHistory(\n store: Store,\n options?: HistoryOptions\n): IHistory {\n return new History(\n store,\n options?.tableName ?? '_history',\n options?.maxActions ?? 200,\n options?.maxAge ?? THIRTY_DAYS,\n options?.generateId ?? defaultGenerateId,\n options?.getAuthor ?? (() => ''),\n options?.withBypass ?? ((fn: () => void) => fn())\n );\n}\n","import { z } from 'zod';\n\n/**\n * Zod schema for the history table.\n * Spread into your store's Zod tables schema to allow history writes.\n *\n * Usage:\n * const myTablesSchema = { ...historyTablesSchema, yourTable: z.object({...}) };\n */\nexport const historyTablesSchema = {\n _history: z.object({\n createdAt: z.number(),\n delta: z.string(),\n dv: z.string(),\n undone: z.number(),\n author: z.string().default(''),\n }),\n};\n"]}
@@ -0,0 +1,277 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/delta.ts
4
+ function deserializeDelta(json) {
5
+ return JSON.parse(json);
6
+ }
7
+ function deserializeDeltaValues(json) {
8
+ return JSON.parse(json);
9
+ }
10
+ function applyDelta(store, delta, dv, index) {
11
+ for (const tableId of Object.keys(delta)) {
12
+ for (const rowId of Object.keys(delta[tableId])) {
13
+ for (const cellId of Object.keys(delta[tableId][rowId])) {
14
+ const value = delta[tableId][rowId][cellId][index];
15
+ if (value == null) {
16
+ store.delCell(tableId, rowId, cellId, true);
17
+ } else {
18
+ store.setCell(tableId, rowId, cellId, value);
19
+ }
20
+ }
21
+ }
22
+ }
23
+ for (const valueId of Object.keys(dv)) {
24
+ const value = dv[valueId][index];
25
+ if (value == null) {
26
+ store.delValue(valueId);
27
+ } else {
28
+ store.setValue(valueId, value);
29
+ }
30
+ }
31
+ }
32
+ function applyReverse(store, delta, dv) {
33
+ applyDelta(store, delta, dv, 0);
34
+ }
35
+ function applyForward(store, delta, dv) {
36
+ applyDelta(store, delta, dv, 1);
37
+ }
38
+
39
+ // src/cleanup.ts
40
+ function runCleanup(store, tableName, maxActions, maxAge, history, listeners, withBypass) {
41
+ const now = Date.now();
42
+ const rowIds = store.getRowIds(tableName);
43
+ const actions = rowIds.map((id) => ({
44
+ id,
45
+ createdAt: store.getCell(tableName, id, "createdAt"),
46
+ undone: store.getCell(tableName, id, "undone"),
47
+ delta: store.getCell(tableName, id, "delta"),
48
+ dv: store.getCell(tableName, id, "dv")
49
+ }));
50
+ actions.sort((a, b) => a.createdAt - b.createdAt);
51
+ const toRemoveSet = /* @__PURE__ */ new Set();
52
+ for (const action of actions) {
53
+ if (now - action.createdAt > maxAge) {
54
+ toRemoveSet.add(action.id);
55
+ }
56
+ }
57
+ const remaining = actions.filter((a) => !toRemoveSet.has(a.id));
58
+ if (remaining.length > maxActions) {
59
+ const excess = remaining.length - maxActions;
60
+ for (let i = 0; i < excess; i++) {
61
+ toRemoveSet.add(remaining[i].id);
62
+ }
63
+ }
64
+ if (toRemoveSet.size === 0) return;
65
+ const toRemove = actions.filter((a) => toRemoveSet.has(a.id));
66
+ const cleaned = toRemove.map((a) => ({
67
+ id: a.id,
68
+ createdAt: a.createdAt,
69
+ undone: a.undone === 1,
70
+ delta: a.delta,
71
+ deltaValues: a.dv
72
+ }));
73
+ for (const listener of Array.from(listeners.values())) {
74
+ listener(history, cleaned);
75
+ }
76
+ withBypass(() => {
77
+ store.transaction(() => {
78
+ for (const action of toRemove) {
79
+ store.delRow(tableName, action.id);
80
+ }
81
+ });
82
+ });
83
+ }
84
+
85
+ // src/history.ts
86
+ var History = class {
87
+ constructor(store, tableName, maxActions, maxAge, generateId, getAuthor, bypassFn) {
88
+ this.store = store;
89
+ this.tableName = tableName;
90
+ this.maxActions = maxActions;
91
+ this.maxAge = maxAge;
92
+ this.generateId = generateId;
93
+ this.getAuthor = getAuthor;
94
+ this.bypassFn = bypassFn;
95
+ this.pendingActionRecorded = false;
96
+ this.cleanupListeners = /* @__PURE__ */ new Map();
97
+ this.nextListenerId = 0;
98
+ this.txListenerId = store.addWillFinishTransactionListener(() => {
99
+ if (!this.pendingAction) return;
100
+ const [, , changedCells, , changedValues] = store.getTransactionLog();
101
+ const filteredCells = { ...changedCells };
102
+ delete filteredCells[this.tableName];
103
+ const hasCellChanges = Object.keys(filteredCells).length > 0;
104
+ const hasValueChanges = Object.keys(changedValues).length > 0;
105
+ if (!hasCellChanges && !hasValueChanges) return;
106
+ const id = this.pendingAction;
107
+ this.bypassFn(() => {
108
+ store.setRow(this.tableName, id, {
109
+ createdAt: Date.now(),
110
+ delta: JSON.stringify(filteredCells),
111
+ dv: JSON.stringify(changedValues),
112
+ undone: 0,
113
+ author: this.getAuthor()
114
+ });
115
+ });
116
+ this.pendingActionRecorded = true;
117
+ });
118
+ }
119
+ action(mutate) {
120
+ const id = this.generateId();
121
+ const undoneIds = this.store.getRowIds(this.tableName).filter(
122
+ (rowId) => this.store.getCell(this.tableName, rowId, "undone") === 1
123
+ );
124
+ if (undoneIds.length > 0) {
125
+ this.bypassFn(() => {
126
+ this.store.transaction(() => {
127
+ for (const rowId of undoneIds) {
128
+ this.store.delRow(this.tableName, rowId);
129
+ }
130
+ });
131
+ });
132
+ }
133
+ this.pendingAction = id;
134
+ this.pendingActionRecorded = false;
135
+ try {
136
+ this.store.transaction(() => mutate());
137
+ } catch (err) {
138
+ this.store.finishTransaction();
139
+ this.pendingAction = void 0;
140
+ throw err;
141
+ }
142
+ this.pendingAction = void 0;
143
+ return this.pendingActionRecorded ? id : void 0;
144
+ }
145
+ goBackward() {
146
+ const [, current] = this.getActionIds();
147
+ if (!current) return this;
148
+ const row = this.store.getRow(this.tableName, current);
149
+ const delta = deserializeDelta(row.delta);
150
+ const dv = deserializeDeltaValues(row.dv);
151
+ this.store.transaction(() => {
152
+ applyReverse(this.store, delta, dv);
153
+ this.bypassFn(() => {
154
+ this.store.setCell(this.tableName, current, "undone", 1);
155
+ });
156
+ });
157
+ return this;
158
+ }
159
+ goForward() {
160
+ const [, , forward] = this.getActionIds();
161
+ if (forward.length === 0) return this;
162
+ const actionId = forward[0];
163
+ const row = this.store.getRow(this.tableName, actionId);
164
+ const delta = deserializeDelta(row.delta);
165
+ const dv = deserializeDeltaValues(row.dv);
166
+ this.store.transaction(() => {
167
+ applyForward(this.store, delta, dv);
168
+ this.bypassFn(() => {
169
+ this.store.setCell(this.tableName, actionId, "undone", 0);
170
+ });
171
+ });
172
+ return this;
173
+ }
174
+ canGoBackward() {
175
+ const [, current] = this.getActionIds();
176
+ return current !== void 0;
177
+ }
178
+ canGoForward() {
179
+ const [, , forward] = this.getActionIds();
180
+ return forward.length > 0;
181
+ }
182
+ getActionIds() {
183
+ const rowIds = this.store.getRowIds(this.tableName);
184
+ const sorted = [...rowIds].sort(
185
+ (a, b) => this.store.getCell(this.tableName, a, "createdAt") - this.store.getCell(this.tableName, b, "createdAt")
186
+ );
187
+ let lastActiveIdx = -1;
188
+ for (let i = sorted.length - 1; i >= 0; i--) {
189
+ if (this.store.getCell(this.tableName, sorted[i], "undone") === 0) {
190
+ lastActiveIdx = i;
191
+ break;
192
+ }
193
+ }
194
+ const backward = [];
195
+ const forward = [];
196
+ for (let i = 0; i < sorted.length; i++) {
197
+ const id = sorted[i];
198
+ const undone = this.store.getCell(this.tableName, id, "undone") === 1;
199
+ if (undone && i > lastActiveIdx) {
200
+ forward.push(id);
201
+ } else if (!undone) {
202
+ backward.push(id);
203
+ }
204
+ }
205
+ const current = backward.pop();
206
+ return [backward, current, forward];
207
+ }
208
+ setMaxActions(max) {
209
+ this.maxActions = max;
210
+ return this;
211
+ }
212
+ setMaxAge(ms) {
213
+ this.maxAge = ms;
214
+ return this;
215
+ }
216
+ cleanup() {
217
+ runCleanup(
218
+ this.store,
219
+ this.tableName,
220
+ this.maxActions,
221
+ this.maxAge,
222
+ this,
223
+ this.cleanupListeners,
224
+ this.bypassFn
225
+ );
226
+ return this;
227
+ }
228
+ addCleanupListener(listener) {
229
+ const id = String(this.nextListenerId++);
230
+ this.cleanupListeners.set(id, listener);
231
+ return id;
232
+ }
233
+ getStore() {
234
+ return this.store;
235
+ }
236
+ delListener(listenerId) {
237
+ this.cleanupListeners.delete(listenerId);
238
+ return this;
239
+ }
240
+ destroy() {
241
+ this.store.delListener(this.txListenerId);
242
+ this.cleanupListeners.clear();
243
+ }
244
+ };
245
+
246
+ // src/create.ts
247
+ var THIRTY_DAYS = 30 * 24 * 60 * 60 * 1e3;
248
+ var counter = 0;
249
+ function defaultGenerateId() {
250
+ const ts = Date.now().toString(36);
251
+ const rand = Math.random().toString(36).slice(2, 8);
252
+ return `${ts}-${rand}-${String(++counter).padStart(4, "0")}`;
253
+ }
254
+ function createHistory(store, options) {
255
+ return new History(
256
+ store,
257
+ options?.tableName ?? "_history",
258
+ options?.maxActions ?? 200,
259
+ options?.maxAge ?? THIRTY_DAYS,
260
+ options?.generateId ?? defaultGenerateId,
261
+ options?.getAuthor ?? (() => ""),
262
+ options?.withBypass ?? ((fn) => fn())
263
+ );
264
+ }
265
+ var historyTablesSchema = {
266
+ _history: z.object({
267
+ createdAt: z.number(),
268
+ delta: z.string(),
269
+ dv: z.string(),
270
+ undone: z.number(),
271
+ author: z.string().default("")
272
+ })
273
+ };
274
+
275
+ export { createHistory, historyTablesSchema };
276
+ //# sourceMappingURL=index.mjs.map
277
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/delta.ts","../src/cleanup.ts","../src/history.ts","../src/create.ts","../src/schema.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { Store } from 'tinybase';\nimport type { ParsedDelta, ParsedDeltaValues } from './types';\n\nexport function deserializeDelta(json: string): ParsedDelta {\n return JSON.parse(json);\n}\n\nexport function deserializeDeltaValues(json: string): ParsedDeltaValues {\n return JSON.parse(json);\n}\n\nfunction applyDelta(\n store: Store,\n delta: ParsedDelta,\n dv: ParsedDeltaValues,\n index: 0 | 1\n): void {\n for (const tableId of Object.keys(delta)) {\n for (const rowId of Object.keys(delta[tableId])) {\n for (const cellId of Object.keys(delta[tableId][rowId])) {\n const value = delta[tableId][rowId][cellId][index];\n if (value == null) {\n store.delCell(tableId, rowId, cellId, true);\n } else {\n store.setCell(tableId, rowId, cellId, value);\n }\n }\n }\n }\n for (const valueId of Object.keys(dv)) {\n const value = dv[valueId][index];\n if (value == null) {\n store.delValue(valueId);\n } else {\n store.setValue(valueId, value);\n }\n }\n}\n\nexport function applyReverse(\n store: Store,\n delta: ParsedDelta,\n dv: ParsedDeltaValues\n): void {\n applyDelta(store, delta, dv, 0);\n}\n\nexport function applyForward(\n store: Store,\n delta: ParsedDelta,\n dv: ParsedDeltaValues\n): void {\n applyDelta(store, delta, dv, 1);\n}\n","import type { Store } from 'tinybase';\nimport type { History, CleanedAction, CleanupListener } from './types';\n\nexport function runCleanup(\n store: Store,\n tableName: string,\n maxActions: number,\n maxAge: number,\n history: History,\n listeners: Map<string, CleanupListener>,\n withBypass: (fn: () => void) => void\n): void {\n const now = Date.now();\n const rowIds = store.getRowIds(tableName);\n\n const actions = rowIds.map((id) => ({\n id,\n createdAt: store.getCell(tableName, id, 'createdAt') as number,\n undone: store.getCell(tableName, id, 'undone') as number,\n delta: store.getCell(tableName, id, 'delta') as string,\n dv: store.getCell(tableName, id, 'dv') as string,\n }));\n\n actions.sort((a, b) => a.createdAt - b.createdAt);\n\n const toRemoveSet = new Set<string>();\n\n for (const action of actions) {\n if (now - action.createdAt > maxAge) {\n toRemoveSet.add(action.id);\n }\n }\n\n const remaining = actions.filter((a) => !toRemoveSet.has(a.id));\n if (remaining.length > maxActions) {\n const excess = remaining.length - maxActions;\n for (let i = 0; i < excess; i++) {\n toRemoveSet.add(remaining[i].id);\n }\n }\n\n if (toRemoveSet.size === 0) return;\n\n const toRemove = actions.filter((a) => toRemoveSet.has(a.id));\n\n const cleaned: CleanedAction[] = toRemove.map((a) => ({\n id: a.id,\n createdAt: a.createdAt,\n undone: a.undone === 1,\n delta: a.delta,\n deltaValues: a.dv,\n }));\n\n for (const listener of Array.from(listeners.values())) {\n listener(history, cleaned);\n }\n\n withBypass(() => {\n store.transaction(() => {\n for (const action of toRemove) {\n store.delRow(tableName, action.id);\n }\n });\n });\n}\n","import type { Store, Id } from 'tinybase';\nimport type { History as IHistory, CleanupListener } from './types';\nimport {\n deserializeDelta,\n deserializeDeltaValues,\n applyReverse,\n applyForward,\n} from './delta';\nimport { runCleanup } from './cleanup';\n\nexport class History implements IHistory {\n private pendingAction: string | undefined;\n private pendingActionRecorded = false;\n private readonly cleanupListeners = new Map<string, CleanupListener>();\n private nextListenerId = 0;\n private txListenerId: Id;\n\n constructor(\n private readonly store: Store,\n private readonly tableName: string,\n private maxActions: number,\n private maxAge: number,\n private readonly generateId: () => string,\n private readonly getAuthor: () => string,\n private readonly bypassFn: (fn: () => void) => void\n ) {\n this.txListenerId = store.addWillFinishTransactionListener(() => {\n if (!this.pendingAction) return;\n\n const [, , changedCells, , changedValues] = store.getTransactionLog();\n\n const filteredCells = { ...changedCells };\n delete filteredCells[this.tableName];\n\n const hasCellChanges = Object.keys(filteredCells).length > 0;\n const hasValueChanges = Object.keys(changedValues).length > 0;\n\n if (!hasCellChanges && !hasValueChanges) return;\n\n const id = this.pendingAction;\n\n this.bypassFn(() => {\n store.setRow(this.tableName, id, {\n createdAt: Date.now(),\n delta: JSON.stringify(filteredCells),\n dv: JSON.stringify(changedValues),\n undone: 0,\n author: this.getAuthor(),\n });\n });\n\n this.pendingActionRecorded = true;\n });\n }\n\n action(mutate: () => void): Id | undefined {\n const id = this.generateId();\n\n const undoneIds = this.store\n .getRowIds(this.tableName)\n .filter(\n (rowId) => this.store.getCell(this.tableName, rowId, 'undone') === 1\n );\n if (undoneIds.length > 0) {\n this.bypassFn(() => {\n this.store.transaction(() => {\n for (const rowId of undoneIds) {\n this.store.delRow(this.tableName, rowId);\n }\n });\n });\n }\n\n this.pendingAction = id;\n this.pendingActionRecorded = false;\n try {\n this.store.transaction(() => mutate());\n } catch (err) {\n // TinyBase skips finishTransaction on throw, leaving the store\n // stuck. Manually finish so listeners fire and the store recovers.\n this.store.finishTransaction();\n this.pendingAction = undefined;\n throw err;\n }\n this.pendingAction = undefined;\n\n return this.pendingActionRecorded ? id : undefined;\n }\n\n goBackward(): this {\n const [, current] = this.getActionIds();\n if (!current) return this;\n\n const row = this.store.getRow(this.tableName, current);\n const delta = deserializeDelta(row.delta as string);\n const dv = deserializeDeltaValues(row.dv as string);\n\n this.store.transaction(() => {\n applyReverse(this.store, delta, dv);\n this.bypassFn(() => {\n this.store.setCell(this.tableName, current, 'undone', 1);\n });\n });\n\n return this;\n }\n\n goForward(): this {\n const [, , forward] = this.getActionIds();\n if (forward.length === 0) return this;\n\n const actionId = forward[0];\n const row = this.store.getRow(this.tableName, actionId);\n const delta = deserializeDelta(row.delta as string);\n const dv = deserializeDeltaValues(row.dv as string);\n\n this.store.transaction(() => {\n applyForward(this.store, delta, dv);\n this.bypassFn(() => {\n this.store.setCell(this.tableName, actionId, 'undone', 0);\n });\n });\n\n return this;\n }\n\n canGoBackward(): boolean {\n const [, current] = this.getActionIds();\n return current !== undefined;\n }\n\n canGoForward(): boolean {\n const [, , forward] = this.getActionIds();\n return forward.length > 0;\n }\n\n getActionIds(): [Id[], Id | undefined, Id[]] {\n const rowIds = this.store.getRowIds(this.tableName);\n const sorted = [...rowIds].sort(\n (a, b) =>\n (this.store.getCell(this.tableName, a, 'createdAt') as number) -\n (this.store.getCell(this.tableName, b, 'createdAt') as number)\n );\n\n let lastActiveIdx = -1;\n for (let i = sorted.length - 1; i >= 0; i--) {\n if (this.store.getCell(this.tableName, sorted[i], 'undone') === 0) {\n lastActiveIdx = i;\n break;\n }\n }\n\n const backward: Id[] = [];\n const forward: Id[] = [];\n\n for (let i = 0; i < sorted.length; i++) {\n const id = sorted[i];\n const undone = this.store.getCell(this.tableName, id, 'undone') === 1;\n if (undone && i > lastActiveIdx) {\n forward.push(id);\n } else if (!undone) {\n backward.push(id);\n }\n }\n\n const current = backward.pop();\n return [backward, current, forward];\n }\n\n setMaxActions(max: number): this {\n this.maxActions = max;\n return this;\n }\n\n setMaxAge(ms: number): this {\n this.maxAge = ms;\n return this;\n }\n\n cleanup(): this {\n runCleanup(\n this.store,\n this.tableName,\n this.maxActions,\n this.maxAge,\n this,\n this.cleanupListeners,\n this.bypassFn\n );\n return this;\n }\n\n addCleanupListener(listener: CleanupListener): Id {\n const id = String(this.nextListenerId++);\n this.cleanupListeners.set(id, listener);\n return id;\n }\n\n getStore(): Store {\n return this.store;\n }\n\n delListener(listenerId: Id): this {\n this.cleanupListeners.delete(listenerId);\n return this;\n }\n\n destroy(): void {\n this.store.delListener(this.txListenerId);\n this.cleanupListeners.clear();\n }\n}\n","import type { Store } from 'tinybase';\nimport type { History as IHistory, HistoryOptions } from './types';\nimport { History } from './history';\n\nconst THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;\n\nlet counter = 0;\nfunction defaultGenerateId(): string {\n const ts = Date.now().toString(36);\n const rand = Math.random().toString(36).slice(2, 8);\n return `${ts}-${rand}-${String(++counter).padStart(4, '0')}`;\n}\n\nexport function createHistory(\n store: Store,\n options?: HistoryOptions\n): IHistory {\n return new History(\n store,\n options?.tableName ?? '_history',\n options?.maxActions ?? 200,\n options?.maxAge ?? THIRTY_DAYS,\n options?.generateId ?? defaultGenerateId,\n options?.getAuthor ?? (() => ''),\n options?.withBypass ?? ((fn: () => void) => fn())\n );\n}\n","import { z } from 'zod';\n\n/**\n * Zod schema for the history table.\n * Spread into your store's Zod tables schema to allow history writes.\n *\n * Usage:\n * const myTablesSchema = { ...historyTablesSchema, yourTable: z.object({...}) };\n */\nexport const historyTablesSchema = {\n _history: z.object({\n createdAt: z.number(),\n delta: z.string(),\n dv: z.string(),\n undone: z.number(),\n author: z.string().default(''),\n }),\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "tinybase-mergeable-history",
3
+ "version": "1.0.0",
4
+ "description": "Persistent, syncable, CRDT-aware undo/redo history for TinyBase MergeableStore. Unlike built-in Checkpoints, history is stored as data inside the store, so it survives reloads and merges across clients.",
5
+ "main": "build/index.js",
6
+ "module": "build/index.mjs",
7
+ "types": "build/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./build/index.d.ts",
11
+ "import": "./build/index.mjs",
12
+ "require": "./build/index.js"
13
+ }
14
+ },
15
+ "files": ["build", "README.md", "LICENSE"],
16
+ "sideEffects": false,
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "test": "vitest run",
20
+ "test:coverage": "vitest run --coverage",
21
+ "type-check": "tsc --noEmit",
22
+ "prepublishOnly": "tsup"
23
+ },
24
+ "keywords": ["tinybase","undo","redo","history","checkpoints","mergeable","mergeablestore","crdt","sync","local-first","undo-redo"],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/InsidePics/tinybase-mergeable-history.git"
28
+ },
29
+ "bugs": { "url": "https://github.com/InsidePics/tinybase-mergeable-history/issues" },
30
+ "homepage": "https://github.com/InsidePics/tinybase-mergeable-history#readme",
31
+ "author": "INSP LLC",
32
+ "license": "MIT",
33
+ "peerDependencies": { "tinybase": "^8.0.0" },
34
+ "dependencies": { "zod": "^4.3.6" },
35
+ "devDependencies": {
36
+ "tinybase": "^8.0.2",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^4.0.18"
40
+ }
41
+ }