mikser-io 9.57.0 → 9.58.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/CLAUDE.md CHANGED
@@ -142,10 +142,11 @@ brevity.
142
142
  schema_version). Nothing durable lives here any more.
143
143
  - `database/durable.js` — the durable store. `registerMigrations()`,
144
144
  `useDurableDatabase()` (a knex instance), `runMigrations()`,
145
- `closeDurableDatabase()`. `adoptFromCache` carries tables out of a
146
- pre-9.56 database, driven by the `durable_tables` record the old design
147
- kept in `mikser_meta` names alone would move any cache table that
148
- happened to collide. `ensureIgnored` adds the file to `.gitignore`.
145
+ `closeDurableDatabase()`. `ensureIgnored` adds the file to `.gitignore`
146
+ (it holds credentials and the working folder is usually a repo).
147
+ **No upgrade path from the pre-split layout** per the posture above,
148
+ a working folder from before 9.56 loses its grants and change-set log
149
+ to the ordinary cache wipe, and everyone signs in again once.
149
150
  `sift-to-sql.js` translates sift filters to SQL WHERE clauses
150
151
  against `INDEXED_COLUMNS`; un-pushed clauses fall through to
151
152
  JS-side sift. `query-context.js` is the AsyncLocalStorage that
@@ -101,7 +101,13 @@ boot can never add a column, rename one, or backfill a value; that is invisible
101
101
  for a cache table and the permanent condition of a durable one.
102
102
 
103
103
  `registerSchema(name, sql, { durable: true })` now THROWS, pointing at
104
- `registerMigrations`. Accepting it would put the table in the file that gets
104
+ `registerMigrations`.
105
+
106
+ No upgrade path from the single-file layout, deliberately. Until v10 mikser
107
+ carries no back-compat, and a one-shot migration for a shape that will never
108
+ exist again is exactly the code that shape does not earn. A working folder
109
+ written before 9.56 loses its grants and change-set log to the ordinary cache
110
+ wipe: everyone signs in once more, and the change-set history starts empty. Accepting it would put the table in the file that gets
105
111
  deleted, and the first sign would be an operator asked to sign in again.
106
112
 
107
113
  For the cache: WAL mode + `synchronous=NORMAL` +
package/index.js CHANGED
@@ -10,12 +10,13 @@ export * from './src/report.js'
10
10
  // CLI formats, rather than each one reimplementing the question.
11
11
  export * from './src/explain.js'
12
12
  export * from './src/lifecycle.js'
13
- export * from './src/database/index.js'
14
13
  // The durable store — registerMigrations / useDurableDatabase. A separate
15
14
  // module from the cache because it is a separate database with none of the
16
15
  // cache's constraints: main-thread only, so async, so knex, so portable to
17
16
  // another engine.
18
17
  export * from './src/database/durable.js'
18
+
19
+ export * from './src/database/index.js'
19
20
  export * from './src/journal.js'
20
21
  export * from './src/catalog.js'
21
22
  export * from './src/search.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.57.0",
3
+ "version": "9.58.0",
4
4
  "description": "A mixer for content: entities in, configurable render pipelines, outputs of any kind. Static sites are the canonical recipe, not the definition — the same engine renders PDFs, emails and whatever a renderer plugin produces. Files are the source of truth, every lifecycle phase is observable, and the build graph is queryable by an agent.",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -28,8 +28,7 @@
28
28
  // ordered list of migrations instead, applied once and recorded.
29
29
 
30
30
  import path from 'node:path'
31
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
32
- import Database from 'better-sqlite3'
31
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
33
32
  import knexFactory from 'knex'
34
33
 
35
34
  import runtime from '../runtime.js'
@@ -190,106 +189,6 @@ export async function runMigrations(knex, logger = useLogger()) {
190
189
  return applied
191
190
  }
192
191
 
193
- // Carry a durable table out of a cache file written before the split.
194
- //
195
- // UPGRADE PATH, and the one piece of this module that knows it is talking
196
- // to sqlite. It has to: moving rows between two files is ATTACH, and there
197
- // is no portable spelling of it. Gated on the durable store actually being
198
- // a local sqlite file, skipped otherwise, and deletable once no deployment
199
- // predates 9.56.
200
- //
201
- // Which tables those are is not guessed. The previous design recorded the
202
- // names in `mikser_meta.durable_tables` — it had to, so that a config which
203
- // never loaded the owning plugin would not unlink them — and that record is
204
- // exactly the upgrade instruction needed here. Matching on names alone would
205
- // be worse than useless: a cache table that happens to share a name with some
206
- // plugin's durable table would be moved and dropped, which is destroying data
207
- // on the strength of a coincidence.
208
- //
209
- // Runs on the DURABLE connection with the cache attached, rather than the
210
- // other way round, so a table the schemas did not create can be rebuilt by
211
- // replaying the CREATE sqlite itself stored — unqualified, landing in durable
212
- // because that is this connection's main. The alternative was rewriting the
213
- // stored DDL to insert a schema prefix, and parsing DDL is what produced
214
- // columns named `and`, `a` and `which` on a live deployment.
215
- //
216
- // The record is cleared once the move succeeds, so this is a one-way door
217
- // that runs once per working folder.
218
- export function adoptFromCache(durable, cachePath, logger) {
219
- if (!cachePath || cachePath === ':memory:' || !existsSync(cachePath)) return
220
-
221
- let recorded = []
222
- try {
223
- durable.exec(`ATTACH DATABASE '${cachePath.replace(/'/g, "''")}' AS cache`)
224
- const row = durable.prepare('SELECT value FROM cache.mikser_meta WHERE key = ?').get('durable_tables')
225
- const parsed = row?.value ? JSON.parse(row.value) : []
226
- recorded = Array.isArray(parsed) ? parsed.filter(name => typeof name === 'string') : []
227
- } catch {
228
- try { durable.exec('DETACH DATABASE cache') } catch { /* never attached */ }
229
- return // no cache, or one that predates the record — nothing to carry
230
- }
231
-
232
- try {
233
- const present = new Set(durable.prepare(
234
- "SELECT name FROM cache.sqlite_master WHERE type='table'").all().map(r => r.name))
235
- const mine = new Set(durable.prepare(
236
- "SELECT name FROM main.sqlite_master WHERE type='table'").all().map(r => r.name))
237
-
238
- for (const table of recorded) {
239
- if (table === 'mikser_meta' || !present.has(table)) continue
240
- try {
241
- // A table no loaded plugin declares still has to come across —
242
- // that is the case the record exists for. Rebuilt from the
243
- // DDL sqlite kept, so no schema has to be reconstructed.
244
- if (!mine.has(table)) {
245
- const { sql } = durable.prepare(
246
- "SELECT sql FROM cache.sqlite_master WHERE type='table' AND name = ?").get(table) ?? {}
247
- if (!sql) continue
248
- durable.exec(sql)
249
- }
250
-
251
- const columnsOf = (schema) =>
252
- durable.prepare(`PRAGMA ${schema}.table_info("${table}")`).all().map(c => c.name)
253
- // Intersected rather than trusting `SELECT *`: the shape in
254
- // the cache is by definition the OLD one, and lining the two
255
- // up wrong would put values in the wrong columns.
256
- const source = new Set(columnsOf('cache'))
257
- const shared = columnsOf('main').filter(c => source.has(c))
258
- const list = shared.map(c => `"${c}"`).join(', ')
259
-
260
- const moved = durable.transaction(() => {
261
- const { n } = durable.prepare(`SELECT COUNT(*) AS n FROM cache."${table}"`).get()
262
- if (n && shared.length) {
263
- durable.exec(
264
- `INSERT OR IGNORE INTO main."${table}" (${list}) SELECT ${list} FROM cache."${table}"`)
265
- }
266
- durable.exec(`DROP TABLE cache."${table}"`)
267
- return n
268
- })()
269
-
270
- logger?.notice('Moved %s out of the cache and into %s (%d row%s)',
271
- table, DEFAULT_DURABLE_FILENAME, moved, moved === 1 ? '' : 's')
272
- } catch (err) {
273
- // Loud, and specifically not fatal: the rows are still in the
274
- // cache, which means the next wipe takes them. That is a
275
- // warning an operator can act on; silence is not.
276
- logger?.error({ code: 'durable-adopt' },
277
- 'Could not move %s into the durable database: %s. Its rows are still in the cache, so the '
278
- + 'next wipe will delete them.', table, err.message)
279
- }
280
- }
281
-
282
- // Cleared last, and only here. While it is present this runs again,
283
- // which is what makes a partial move recoverable on the next start.
284
- durable.prepare('DELETE FROM cache.mikser_meta WHERE key = ?').run('durable_tables')
285
- } catch (err) {
286
- logger?.error({ code: 'durable-adopt' },
287
- 'Could not read the durable-table record out of the cache: %s', err.message)
288
- } finally {
289
- try { durable.exec('DETACH DATABASE cache') } catch { /* already detached */ }
290
- }
291
- }
292
-
293
192
  // Keep the durable database out of the repository.
294
193
  //
295
194
  // It sits at the working-folder root — deliberately outside runtime/, which
@@ -324,26 +223,6 @@ export function ensureIgnored(folder, filename, logger) {
324
223
  }
325
224
  }
326
225
 
327
- // Carry pre-split tables across, on a connection of its own.
328
- //
329
- // Raw better-sqlite3 rather than the knex instance because the move is ATTACH,
330
- // which knex has no portable spelling for — and because this is an upgrade
331
- // path with an end date, not part of how the store works. After migrations, so
332
- // the tables it copies into exist with their current shape.
333
- function adoptPreSplitTables(durablePath, cachePath, logger) {
334
- if (!existsSync(cachePath)) return
335
- let handle = null
336
- try {
337
- handle = new Database(durablePath)
338
- adoptFromCache(handle, cachePath, logger)
339
- } catch (err) {
340
- logger?.error({ code: 'durable-adopt' },
341
- 'Could not carry pre-split tables out of the cache: %s', err.message)
342
- } finally {
343
- try { handle?.close() } catch { /* already closed */ }
344
- }
345
- }
346
-
347
226
  onLoaded(async () => {
348
227
  if (db) return // watch mode keeps the connection across cycles
349
228
  if (!registry.size) return // nothing durable is registered; open nothing
@@ -370,11 +249,6 @@ onLoaded(async () => {
370
249
  return
371
250
  }
372
251
 
373
- // Only a local sqlite target can have a pre-split cache beside it.
374
- if (filename && runtime.options.runtimeFolder) {
375
- adoptPreSplitTables(filename, path.join(runtime.options.runtimeFolder, 'mikser.sqlite'), logger)
376
- }
377
-
378
252
  runtime.durable = db
379
253
  logger?.info('Durable store ready: %s (%s)', filename ?? 'remote', config.client)
380
254
  })