mikser-io 6.9.4 → 6.15.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 +2 -1
- package/src/api.js +51 -12
- package/src/journal.js +7 -1
- package/src/lifecycle.js +9 -0
- package/src/plugins/api.js +15 -3
- package/src/plugins/layouts.js +26 -7
- package/src/tracking.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.15.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
|
|
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 {
|
|
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
|
|
60
|
+
await runtime.update(item.entity).catch(item.reject)
|
|
65
61
|
}
|
|
66
62
|
await runtime.process()
|
|
67
63
|
} catch (err) {
|
|
@@ -91,16 +87,41 @@ 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
|
+
* Two control flags mirror mikser's default-keep-everything behavior;
|
|
91
|
+
* both opt-out via strict `=== false`:
|
|
92
|
+
*
|
|
93
|
+
* - `catalog: true` (default) — keep the entity in the catalog after
|
|
94
|
+
* the render. Pass `catalog: false` to prune the catalog row;
|
|
95
|
+
* useful for on-demand renders where the metadata row would just
|
|
96
|
+
* accumulate.
|
|
97
|
+
* - `save: true` (default) — write the rendered output to disk at
|
|
98
|
+
* `<outputFolder>/<entity.destination>`. Pass `save: false` to
|
|
99
|
+
* skip the final disk write; the bytes still come back via
|
|
100
|
+
* `output.result` for you to pipe wherever you want (HTTP
|
|
101
|
+
* response, S3, …). For layouts with a postprocessor (e.g.
|
|
102
|
+
* `*.html-pdf.*`), the intermediate file is still written so the
|
|
103
|
+
* postprocessor can consume it; only the FINAL output is skipped.
|
|
104
|
+
*
|
|
105
|
+
* The rendered output's bytes are always returned in `output.result`
|
|
106
|
+
* regardless of either flag — `save` only affects whether they also
|
|
107
|
+
* end up on disk.
|
|
108
|
+
*
|
|
94
109
|
* @param {object} entity - any entity-shaped object
|
|
95
110
|
* @param {object} [opts]
|
|
96
|
-
* @param {number}
|
|
111
|
+
* @param {number} [opts.timeout] - override the default timeout
|
|
112
|
+
* @param {boolean} [opts.catalog=true] - keep the catalog row after render
|
|
113
|
+
* @param {boolean} [opts.save=true] - write the rendered output to disk
|
|
97
114
|
* @returns {Promise<{output, entity}>}
|
|
98
115
|
*/
|
|
99
|
-
function render(entity, { timeout = defaultTimeout } = {}) {
|
|
100
|
-
|
|
116
|
+
async function render(entity, { timeout = defaultTimeout, catalog = true, save = true } = {}) {
|
|
117
|
+
const result = await new Promise((resolve, reject) => {
|
|
101
118
|
const correlationId = randomUUID()
|
|
119
|
+
const stamped = { ...entity, _correlationId: correlationId }
|
|
120
|
+
// Only stamp _save when explicitly opting out — keeps the
|
|
121
|
+
// entity object clean for the common case.
|
|
122
|
+
if (save === false) stamped._save = false
|
|
102
123
|
pending.push({
|
|
103
|
-
entity:
|
|
124
|
+
entity: stamped,
|
|
104
125
|
correlationId,
|
|
105
126
|
timeout,
|
|
106
127
|
resolve,
|
|
@@ -109,6 +130,24 @@ export function useRenderer(runtime, {
|
|
|
109
130
|
})
|
|
110
131
|
if (!cycleRunning) setImmediate(runBatch)
|
|
111
132
|
})
|
|
133
|
+
|
|
134
|
+
if (catalog === false) {
|
|
135
|
+
// Explicit opt-out: prune the catalog row so it doesn't
|
|
136
|
+
// accumulate. The rendered output file stays on disk — the
|
|
137
|
+
// bytes are the work product. We deliberately bypass the
|
|
138
|
+
// journal/DELETE path here (which would also unlink the file
|
|
139
|
+
// via engine.js's manifest cleanup) and splice the entity
|
|
140
|
+
// out of the in-memory catalog directly. Strict equality so
|
|
141
|
+
// ambiguous inputs (null, "false", 0) fall through to the
|
|
142
|
+
// default of keeping the row.
|
|
143
|
+
const entities = runtime.catalog?.data?.entities
|
|
144
|
+
if (entities) {
|
|
145
|
+
const idx = entities.findIndex(e => e.id === result.entity.id)
|
|
146
|
+
if (idx >= 0) entities.splice(idx, 1)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result
|
|
112
151
|
}
|
|
113
152
|
|
|
114
153
|
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
|
-
|
|
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 }
|
package/src/plugins/api.js
CHANGED
|
@@ -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,16 @@ export default ({
|
|
|
150
148
|
|
|
151
149
|
router.post('/render', auth, async (req, res) => {
|
|
152
150
|
try {
|
|
153
|
-
|
|
151
|
+
// Body shape mirrors the JS API: entity fields at top
|
|
152
|
+
// level, control flags grouped under `options`. Forwarded
|
|
153
|
+
// straight to render(entity, options). Defaults match
|
|
154
|
+
// mikser's lifecycle (save and keep the catalog row);
|
|
155
|
+
// strict opt-outs via the literal `false`:
|
|
156
|
+
// options.catalog: false → prune the catalog row
|
|
157
|
+
// options.save: false → skip the final disk write
|
|
158
|
+
// (bytes still in the response)
|
|
159
|
+
const { options = {}, ...entityShape } = req.body
|
|
160
|
+
const { output, entity } = await render(entityShape, options)
|
|
154
161
|
await sendRenderOutput(res, output, entity)
|
|
155
162
|
} catch (err) {
|
|
156
163
|
logger.error('Api render error: %s', err.message)
|
|
@@ -160,6 +167,11 @@ export default ({
|
|
|
160
167
|
}
|
|
161
168
|
})
|
|
162
169
|
|
|
170
|
+
// The HTTP server keeps the process alive across many process()
|
|
171
|
+
// cycles. Tell the journal layer not to tear down the sqlite
|
|
172
|
+
// connection at the end of each cycle.
|
|
173
|
+
runtime.options.persistent = true
|
|
174
|
+
|
|
163
175
|
const base = runtime.config.api?.base ?? '/api'
|
|
164
176
|
app.use(base, router)
|
|
165
177
|
|
package/src/plugins/layouts.js
CHANGED
|
@@ -370,13 +370,32 @@ export default ({
|
|
|
370
370
|
onComplete(async ({ entity, options, output }) => {
|
|
371
371
|
const logger = useLogger()
|
|
372
372
|
if (entity.layout && !options?.ignore && output.result != null) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
373
|
+
// `_save: false` (stamped by useRenderer when called with
|
|
374
|
+
// { save: false }) opts out of writing the final output to
|
|
375
|
+
// disk. The bytes still come back to the caller via
|
|
376
|
+
// output.result. Strict equality — only the literal `false`
|
|
377
|
+
// opts out, matching the catalog-flag pattern.
|
|
378
|
+
//
|
|
379
|
+
// The intermediate file (when a postprocessor will run next)
|
|
380
|
+
// must still be written so the postprocessor can consume it,
|
|
381
|
+
// so we only honour `_save: false` for the FINAL output —
|
|
382
|
+
// detected as either "no postprocessor configured" or "we're
|
|
383
|
+
// running on the postprocess side already" (origin set).
|
|
384
|
+
const isFinal = !entity.layout.postprocessor || entity.origin != null
|
|
385
|
+
const skipWrite = entity._save === false && isFinal
|
|
386
|
+
|
|
387
|
+
if (!skipWrite) {
|
|
388
|
+
const destinationFile = path.join(runtime.options.outputFolder, entity.destination)
|
|
389
|
+
await mkdir(path.dirname(destinationFile), { recursive: true })
|
|
390
|
+
try {
|
|
391
|
+
await unlink(destinationFile)
|
|
392
|
+
} catch { }
|
|
393
|
+
await writeFile(destinationFile, output.result)
|
|
394
|
+
logger.debug('Layout render finished: %s', entity.destination.replace(runtime.options.workingFolder, ''))
|
|
395
|
+
} else {
|
|
396
|
+
logger.debug('Layout render finished (save:false, bytes only): %s', entity.id)
|
|
397
|
+
}
|
|
398
|
+
|
|
380
399
|
if (entity.origin && entity.origin !== entity.destination) {
|
|
381
400
|
// Don't unlink the origin if it was the same path we just
|
|
382
401
|
// wrote to (post plugins that produce the same extension as
|
package/src/tracking.js
CHANGED
|
@@ -55,7 +55,7 @@ export function stopProgress() {
|
|
|
55
55
|
logger.warn('%s unfinished: %d', name, total - value)
|
|
56
56
|
} else {
|
|
57
57
|
const time = Math.round((Date.now() - progress.stamp) / 1000)
|
|
58
|
-
logger.info('%s: %s', name, formatTime(time, { autopaddingChar: '' }))
|
|
58
|
+
logger.info('%s finished: %d %s', name, total, formatTime(time, { autopaddingChar: '' }))
|
|
59
59
|
}
|
|
60
60
|
progress = {}
|
|
61
61
|
}
|