mikser-io 9.48.0 → 9.49.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/docs/api-reference.md +10 -0
- package/package.json +1 -1
- package/src/changeset.js +58 -8
package/docs/api-reference.md
CHANGED
|
@@ -605,6 +605,16 @@ clearChangeSets(['req-42'])
|
|
|
605
605
|
| `findChangeSet(id)` | resolve one id |
|
|
606
606
|
| `pendingChangeSets()` | sets no consumer has recorded yet, oldest first |
|
|
607
607
|
| `markChangeSetsRecorded(ids, recordedAs)` | mark recorded, and say what as |
|
|
608
|
+
| `closeChangeSet(id)` | the writer is finished with this set |
|
|
609
|
+
|
|
610
|
+
`withChangeSet` takes `closeOnReturn` for the case where the call IS the whole
|
|
611
|
+
request — true whenever the id was minted for it rather than supplied. That is
|
|
612
|
+
exact, not a heuristic: an id nobody else can name cannot grow after the call
|
|
613
|
+
that owns it returns, so a consumer can act on it at once instead of waiting to
|
|
614
|
+
see whether more writes arrive. A caller-supplied id exists so several calls
|
|
615
|
+
can join one set, so it stays open and closes on going quiet. Closing happens
|
|
616
|
+
even when the request throws — work that landed before the failure is real, and
|
|
617
|
+
a set left open forever holds it out of reach.
|
|
608
618
|
|
|
609
619
|
The log is **durable** and survives a restart: nothing else can reconstruct
|
|
610
620
|
which writes belonged to one request. Not the files, which show the result and
|
package/package.json
CHANGED
package/src/changeset.js
CHANGED
|
@@ -41,6 +41,15 @@ registerSchema('change_sets', `
|
|
|
41
41
|
principal TEXT,
|
|
42
42
|
undo_of TEXT,
|
|
43
43
|
created_at INTEGER NOT NULL,
|
|
44
|
+
-- Set when the writer said it was finished. A set that closed is
|
|
45
|
+
-- committable now; one still open is waiting to see whether more
|
|
46
|
+
-- writes join it.
|
|
47
|
+
closed_at INTEGER,
|
|
48
|
+
-- When the set last grew. A set is one request, and a request is
|
|
49
|
+
-- finished when it stops writing — which is the only signal available,
|
|
50
|
+
-- since a caller grouping several tool calls under one id has not said
|
|
51
|
+
-- which call is the last.
|
|
52
|
+
updated_at INTEGER,
|
|
44
53
|
-- Set when a consumer has durably recorded the set somewhere of its
|
|
45
54
|
-- own — a commit, a snapshot. Until then the set is real and listable
|
|
46
55
|
-- but there is nothing to revert FROM, which is a different answer
|
|
@@ -77,14 +86,48 @@ const changeSetContext = new AsyncLocalStorage()
|
|
|
77
86
|
|
|
78
87
|
// Run `fn` with a change set in effect. Writes inside it are attributed to
|
|
79
88
|
// that set unless they name a different one explicitly.
|
|
89
|
+
//
|
|
90
|
+
// `closeOnReturn` says this call IS the whole request — which is true whenever
|
|
91
|
+
// the id was minted for it rather than supplied by the caller. That is a
|
|
92
|
+
// precise signal, not a heuristic: a set nobody else can name cannot grow
|
|
93
|
+
// after the call that owns it returns, so it is committable immediately.
|
|
94
|
+
//
|
|
95
|
+
// A caller-supplied id is the opposite: it exists so several calls can join
|
|
96
|
+
// one set, and nothing in this call knows whether another is coming. Those
|
|
97
|
+
// close on going quiet instead.
|
|
80
98
|
export function withChangeSet(set, fn) {
|
|
81
99
|
if (!set?.changeSet) return fn()
|
|
82
|
-
|
|
100
|
+
const context = {
|
|
83
101
|
changeSet: set.changeSet,
|
|
84
102
|
summary: set.summary ?? null,
|
|
85
103
|
principal: set.principal ?? null,
|
|
86
104
|
undoOf: set.undoOf ?? null,
|
|
87
|
-
}
|
|
105
|
+
}
|
|
106
|
+
if (!set.closeOnReturn) return changeSetContext.run(context, fn)
|
|
107
|
+
return changeSetContext.run(context, async () => {
|
|
108
|
+
try {
|
|
109
|
+
return await fn()
|
|
110
|
+
} finally {
|
|
111
|
+
// In `finally`: a request that failed part way still wrote what it
|
|
112
|
+
// wrote, and leaving that set open forever would hold real work
|
|
113
|
+
// out of the log's committable half.
|
|
114
|
+
closeChangeSet(set.changeSet)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Mark a set finished. Idempotent, and silent for an id nothing recorded —
|
|
120
|
+
// a request that wrote nothing has no set to close.
|
|
121
|
+
export function closeChangeSet(id) {
|
|
122
|
+
if (!id) return
|
|
123
|
+
const at = Date.now()
|
|
124
|
+
const set = memory.get(id)
|
|
125
|
+
if (set) set.closedAt = at
|
|
126
|
+
const handle = db()
|
|
127
|
+
if (!handle) return
|
|
128
|
+
try {
|
|
129
|
+
handle.prepare('UPDATE mikser_change_sets SET closed_at = COALESCE(closed_at, ?) WHERE id = ?').run(at, id)
|
|
130
|
+
} catch { /* memory still holds it */ }
|
|
88
131
|
}
|
|
89
132
|
|
|
90
133
|
export function currentChangeSet() {
|
|
@@ -109,15 +152,16 @@ function db() {
|
|
|
109
152
|
|
|
110
153
|
function persist(handle, set, rel, operation, entityId) {
|
|
111
154
|
handle.prepare(`
|
|
112
|
-
INSERT INTO mikser_change_sets (id, summary, principal, undo_of, created_at)
|
|
113
|
-
VALUES (@id, @summary, @principal, @undoOf, @createdAt)
|
|
155
|
+
INSERT INTO mikser_change_sets (id, summary, principal, undo_of, created_at, updated_at)
|
|
156
|
+
VALUES (@id, @summary, @principal, @undoOf, @createdAt, @updatedAt)
|
|
114
157
|
ON CONFLICT(id) DO UPDATE SET
|
|
115
|
-
summary
|
|
116
|
-
principal
|
|
117
|
-
undo_of
|
|
158
|
+
summary = COALESCE(mikser_change_sets.summary, excluded.summary),
|
|
159
|
+
principal = COALESCE(mikser_change_sets.principal, excluded.principal),
|
|
160
|
+
undo_of = COALESCE(mikser_change_sets.undo_of, excluded.undo_of),
|
|
161
|
+
updated_at = excluded.updated_at
|
|
118
162
|
`).run({
|
|
119
163
|
id: set.id, summary: set.summary, principal: set.principal,
|
|
120
|
-
undoOf: set.undoOf, createdAt: set.startedAt,
|
|
164
|
+
undoOf: set.undoOf, createdAt: set.startedAt, updatedAt: set.updatedAt,
|
|
121
165
|
})
|
|
122
166
|
handle.prepare(`
|
|
123
167
|
INSERT INTO mikser_change_set_paths (change_set, path, operation, entity_id)
|
|
@@ -152,6 +196,8 @@ function rowsToSets(handle, rows) {
|
|
|
152
196
|
principal: row.principal,
|
|
153
197
|
undoOf: row.undo_of,
|
|
154
198
|
startedAt: row.created_at,
|
|
199
|
+
updatedAt: row.updated_at ?? row.created_at,
|
|
200
|
+
closed: row.closed_at != null,
|
|
155
201
|
recordedAt: row.recorded_at ?? null,
|
|
156
202
|
recordedAs: row.recorded_as ?? null,
|
|
157
203
|
paths: paths.map(p => p.path),
|
|
@@ -167,6 +213,8 @@ function memorySets(filter = () => true) {
|
|
|
167
213
|
principal: set.principal,
|
|
168
214
|
undoOf: set.undoOf,
|
|
169
215
|
startedAt: set.startedAt,
|
|
216
|
+
updatedAt: set.updatedAt ?? set.startedAt,
|
|
217
|
+
closed: Boolean(set.closedAt),
|
|
170
218
|
recordedAt: set.recordedAt ?? null,
|
|
171
219
|
recordedAs: set.recordedAs ?? null,
|
|
172
220
|
paths: [...set.paths.keys()],
|
|
@@ -222,6 +270,7 @@ export function recordChangeSetWrite({
|
|
|
222
270
|
// privileged operation that rewrites the record.
|
|
223
271
|
undoOf: undoOf ?? null,
|
|
224
272
|
startedAt: Date.now(),
|
|
273
|
+
updatedAt: Date.now(),
|
|
225
274
|
paths: new Map(),
|
|
226
275
|
}
|
|
227
276
|
memory.set(changeSet, set)
|
|
@@ -229,6 +278,7 @@ export function recordChangeSetWrite({
|
|
|
229
278
|
if (!set.summary && summary) set.summary = summary
|
|
230
279
|
if (!set.principal && principal) set.principal = principal
|
|
231
280
|
if (!set.undoOf && undoOf) set.undoOf = undoOf
|
|
281
|
+
set.updatedAt = Date.now()
|
|
232
282
|
set.paths.set(rel, operation)
|
|
233
283
|
|
|
234
284
|
const handle = db()
|