mikser-io 6.9.3 → 6.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "6.9.3",
3
+ "version": "6.12.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
  "scripts": {
@@ -57,6 +57,7 @@
57
57
  "devDependencies": {
58
58
  "fluent-ffmpeg": "^2.1.3",
59
59
  "mikser-io-post-mjml": "file:../mikser-io-post-mjml",
60
+ "mikser-io-render-eta": "file:../mikser-io-render-eta",
60
61
  "mikser-io-render-liquid": "file:../mikser-io-render-liquid",
61
62
  "mikser-io-render-markdown": "file:../mikser-io-render-markdown",
62
63
  "sharp": "^0.34.5"
package/src/api.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { randomUUID } from 'node:crypto'
6
6
  import { writeFile, unlink, mkdir } from 'node:fs/promises'
7
7
  import path from 'node:path'
8
- import { updateEntity as defaultUpdateEntity } from './lifecycle.js'
8
+ import './lifecycle.js' // side-effect: attaches runtime.create / update / delete
9
9
 
10
10
  /**
11
11
  * Bind to the runtime and return an on-demand renderer that pipelines
@@ -20,14 +20,10 @@ import { updateEntity as defaultUpdateEntity } from './lifecycle.js'
20
20
  *
21
21
  * @param {object} runtime - the mikser runtime singleton
22
22
  * @param {object} [opts]
23
- * @param {Function} [opts.updateEntity] - override lifecycle.updateEntity (mostly for testing)
24
- * @param {number} [opts.defaultTimeout] - per-render timeout in ms (default 30_000)
23
+ * @param {number} [opts.defaultTimeout] - per-render timeout in ms (default 30_000)
25
24
  * @returns {{ render: (entity, { timeout? }?) => Promise<{ output, entity }> }}
26
25
  */
27
- export function useRenderer(runtime, {
28
- updateEntity = defaultUpdateEntity,
29
- defaultTimeout = 30_000,
30
- } = {}) {
26
+ export function useRenderer(runtime, { defaultTimeout = 30_000 } = {}) {
31
27
  let pending = []
32
28
  let cycleRunning = false
33
29
 
@@ -61,7 +57,7 @@ export function useRenderer(runtime, {
61
57
 
62
58
  try {
63
59
  for (const item of batch) {
64
- await updateEntity(item.entity).catch(item.reject)
60
+ await runtime.update(item.entity).catch(item.reject)
65
61
  }
66
62
  await runtime.process()
67
63
  } catch (err) {
@@ -91,13 +87,21 @@ export function useRenderer(runtime, {
91
87
  * `runtime.process()` cycle — within that cycle, mikser's worker pool
92
88
  * renders the batch in parallel.
93
89
  *
90
+ * By default the entity is **not** kept in the catalog: after the
91
+ * render resolves, the catalog row is pruned so it doesn't accumulate.
92
+ * The rendered output file is always kept on disk — the bytes are the
93
+ * work product. Pass `catalog: true` to also keep the catalog row,
94
+ * useful if you'll re-render the same entity later or query it via
95
+ * `findEntities`.
96
+ *
94
97
  * @param {object} entity - any entity-shaped object
95
98
  * @param {object} [opts]
96
- * @param {number} [opts.timeout] - override the default timeout
99
+ * @param {number} [opts.timeout] - override the default timeout
100
+ * @param {boolean} [opts.catalog=false] - keep the catalog row after render
97
101
  * @returns {Promise<{output, entity}>}
98
102
  */
99
- function render(entity, { timeout = defaultTimeout } = {}) {
100
- return new Promise((resolve, reject) => {
103
+ async function render(entity, { timeout = defaultTimeout, catalog = false } = {}) {
104
+ const result = await new Promise((resolve, reject) => {
101
105
  const correlationId = randomUUID()
102
106
  pending.push({
103
107
  entity: { ...entity, _correlationId: correlationId },
@@ -109,6 +113,22 @@ export function useRenderer(runtime, {
109
113
  })
110
114
  if (!cycleRunning) setImmediate(runBatch)
111
115
  })
116
+
117
+ if (!catalog) {
118
+ // Prune the catalog row so it doesn't accumulate, but LEAVE
119
+ // the rendered output file on disk — the bytes are the work
120
+ // product. We deliberately bypass the journal/DELETE path
121
+ // here (which would also unlink the file via engine.js's
122
+ // manifest cleanup) and splice the entity out of the
123
+ // in-memory catalog directly.
124
+ const entities = runtime.catalog?.data?.entities
125
+ if (entities) {
126
+ const idx = entities.findIndex(e => e.id === result.entity.id)
127
+ if (idx >= 0) entities.splice(idx, 1)
128
+ }
129
+ }
130
+
131
+ return result
112
132
  }
113
133
 
114
134
  return { render }
package/src/journal.js CHANGED
@@ -69,7 +69,13 @@ export async function* useJournal(name, operations, signal) {
69
69
  export async function clearJournal(aborted) {
70
70
  await journal('operations').del()
71
71
  if (!aborted) {
72
- if (runtime.options.watch !== true) {
72
+ // Tear down the sqlite connection only when this is genuinely a
73
+ // one-shot run that's about to exit. Watch mode and any plugin
74
+ // that keeps the process alive (e.g. the API plugin's HTTP server)
75
+ // sets `persistent` so subsequent cycles can still write to the
76
+ // journal — without this, the second /render request would crash
77
+ // with "Unable to acquire a connection".
78
+ if (runtime.options.watch !== true && runtime.options.persistent !== true) {
73
79
  journal.destroy()
74
80
  }
75
81
  }
package/src/lifecycle.js CHANGED
@@ -34,6 +34,15 @@ export async function updateEntity(entity) {
34
34
  }
35
35
  }
36
36
 
37
+ // Expose the three entity operations as methods on the runtime so library
38
+ // code (useRenderer, custom plugins) can call `runtime.update(entity)`
39
+ // instead of importing and threading the standalone functions. The
40
+ // imports above already pulled the runtime singleton into scope; this is
41
+ // just a single assignment per name.
42
+ runtime.create = createEntity
43
+ runtime.update = updateEntity
44
+ runtime.delete = deleteEntity
45
+
37
46
  export async function postprocessEntity(entity, options = {}, context = {}) {
38
47
  const logger = useLogger()
39
48
  const entry = { operation: OPERATION.POSTPROCESS, entity, options, context }
@@ -68,7 +68,6 @@ export default ({
68
68
  runtime,
69
69
  onLoaded,
70
70
  useLogger,
71
- updateEntity,
72
71
  findEntities,
73
72
  }) => {
74
73
  onLoaded(async () => {
@@ -95,7 +94,6 @@ export default ({
95
94
  // library entry point and the Api endpoints share the exact same
96
95
  // batching/timeouts/error semantics.
97
96
  const { render } = useRenderer(runtime, {
98
- updateEntity,
99
97
  defaultTimeout: runtime.config.api?.renderTimeout ?? 30_000,
100
98
  })
101
99
 
@@ -150,7 +148,13 @@ export default ({
150
148
 
151
149
  router.post('/render', auth, async (req, res) => {
152
150
  try {
153
- const { output, entity } = await render(req.body)
151
+ // Pull `catalog` out as a control flag; everything else
152
+ // is treated as the entity. Default is NOT to keep the
153
+ // catalog row (the rendered output stays on disk either
154
+ // way). Send `catalog: true` to keep the row, e.g. if
155
+ // you'll re-render or query the entity later.
156
+ const { catalog, ...entityShape } = req.body
157
+ const { output, entity } = await render(entityShape, { catalog })
154
158
  await sendRenderOutput(res, output, entity)
155
159
  } catch (err) {
156
160
  logger.error('Api render error: %s', err.message)
@@ -160,6 +164,11 @@ export default ({
160
164
  }
161
165
  })
162
166
 
167
+ // The HTTP server keeps the process alive across many process()
168
+ // cycles. Tell the journal layer not to tear down the sqlite
169
+ // connection at the end of each cycle.
170
+ runtime.options.persistent = true
171
+
163
172
  const base = runtime.config.api?.base ?? '/api'
164
173
  app.use(base, router)
165
174
 
package/src/tracking.js CHANGED
@@ -27,6 +27,7 @@ onInitialized(() => {
27
27
  })
28
28
 
29
29
  export function trackProgress(name, total) {
30
+ if (!name || !total) return
30
31
  const logger = useLogger()
31
32
  logger.debug('%s started: %d', name, total)
32
33
  progress.bar?.stop()
@@ -54,7 +55,7 @@ export function stopProgress() {
54
55
  logger.warn('%s unfinished: %d', name, total - value)
55
56
  } else {
56
57
  const time = Math.round((Date.now() - progress.stamp) / 1000)
57
- logger.info('%s: %s', name, formatTime(time, { autopaddingChar: '' }))
58
+ logger.info('%s finished: %d %s', name, total, formatTime(time, { autopaddingChar: '' }))
58
59
  }
59
60
  progress = {}
60
61
  }