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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.48.0",
3
+ "version": "9.49.0",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {
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
- return changeSetContext.run({
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
- }, fn)
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 = COALESCE(mikser_change_sets.summary, excluded.summary),
116
- principal = COALESCE(mikser_change_sets.principal, excluded.principal),
117
- undo_of = COALESCE(mikser_change_sets.undo_of, excluded.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()