omkit 0.0.7 → 0.0.8
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/dist/actions/tail-log.js +12 -16
- package/dist/core/output.js +12 -2
- package/dist/core/run.js +9 -2
- package/package.json +1 -1
package/dist/actions/tail-log.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { watch as fsWatch } from "node:fs";
|
|
2
2
|
import { open, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
5
|
import { action } from "../core/action.js";
|
|
5
6
|
/**
|
|
6
7
|
* Tails a (possibly not-yet-existing) text/log file and streams each appended
|
|
@@ -23,15 +24,13 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
|
|
|
23
24
|
const { source = name, level = "info", fromStart = false, encoding = "utf8", debounceMs = 50, } = opts;
|
|
24
25
|
let pos = 0; // bytes consumed so far
|
|
25
26
|
let pending = ""; // partial trailing line, buffered until its newline
|
|
27
|
+
// Decodes bytes to text across reads. It holds back an incomplete trailing
|
|
28
|
+
// multi-byte char (split across a chunk or read boundary) until its remaining
|
|
29
|
+
// bytes arrive, so a character straddling a boundary never decodes to `�`.
|
|
30
|
+
let decoder = new StringDecoder(encoding);
|
|
26
31
|
// Append every complete line in `text`, keeping any remainder for next time.
|
|
27
32
|
const feed = (text) => {
|
|
28
|
-
|
|
29
|
-
// or leave a rotated file padded with \0, and a NUL run contains no newline —
|
|
30
|
-
// so without this the whole padded region buffers and flushes as one huge
|
|
31
|
-
// bogus line (the tell-tale "line of blanks/spaces"). NUL is never valid in
|
|
32
|
-
// a text log, so dropping it is safe; a NUL-only stretch collapses to "" and
|
|
33
|
-
// is skipped by the empty-line guard below.
|
|
34
|
-
pending += text.replace(/\0/g, "");
|
|
33
|
+
pending += text;
|
|
35
34
|
let newline;
|
|
36
35
|
while ((newline = pending.indexOf("\n")) !== -1) {
|
|
37
36
|
const line = pending.slice(0, newline).replace(/\r$/, "");
|
|
@@ -49,8 +48,11 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
|
|
|
49
48
|
return; // missing/unreadable — wait for the next event
|
|
50
49
|
}
|
|
51
50
|
if (size < pos) {
|
|
51
|
+
// Truncation/rotation: restart from the top and drop any buffered partial
|
|
52
|
+
// line and half-decoded char — they belong to the old file's bytes.
|
|
52
53
|
pos = 0;
|
|
53
54
|
pending = "";
|
|
55
|
+
decoder = new StringDecoder(encoding);
|
|
54
56
|
}
|
|
55
57
|
if (size <= pos)
|
|
56
58
|
return;
|
|
@@ -64,15 +66,9 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
|
|
|
64
66
|
if (bytesRead === 0)
|
|
65
67
|
break;
|
|
66
68
|
pos += bytesRead;
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
for (let i = 0; i < bytesRead; i++) {
|
|
71
|
-
if (buffer[i] !== 0)
|
|
72
|
-
buffer[out++] = buffer[i];
|
|
73
|
-
}
|
|
74
|
-
if (out > 0)
|
|
75
|
-
feed(buffer.subarray(0, out).toString(encoding));
|
|
69
|
+
// decoder.write returns only fully-decoded chars, buffering any partial
|
|
70
|
+
// trailing multi-byte char for the next chunk/read.
|
|
71
|
+
feed(decoder.write(buffer.subarray(0, bytesRead)));
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
finally {
|
package/dist/core/output.js
CHANGED
|
@@ -113,10 +113,20 @@ export const streamLog = async (logs) => {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
finally {
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
// End AND wait for both streams to fully close before resolving. {@link writeLog}
|
|
117
|
+
// truncates and rewrites these same files at finalize; if a buffered chunk from
|
|
118
|
+
// this stream landed *after* that truncation, the OS would zero-fill the gap —
|
|
119
|
+
// a multi-MB run of NUL bytes in run.log. `run` awaits this promise before
|
|
120
|
+
// calling `writeLog`, so this writer is guaranteed done before the rewrite.
|
|
121
|
+
await Promise.all([streamClosed(jsonl), streamClosed(pretty)]);
|
|
118
122
|
}
|
|
119
123
|
};
|
|
124
|
+
/** Resolve once a write stream has flushed and closed its file descriptor. */
|
|
125
|
+
const streamClosed = (stream) => new Promise((resolve) => {
|
|
126
|
+
stream.on("close", () => resolve());
|
|
127
|
+
stream.on("error", () => resolve()); // a stream error still frees the file; don't hang
|
|
128
|
+
stream.end();
|
|
129
|
+
});
|
|
120
130
|
let seq = 0;
|
|
121
131
|
/** Serialize a value for a snapshot file, tolerating circular refs / non-JSON values. */
|
|
122
132
|
const serialize = (value) => {
|
package/dist/core/run.js
CHANGED
|
@@ -126,8 +126,11 @@ export function run(...actions) {
|
|
|
126
126
|
process.on("unhandledRejection", onUnhandled);
|
|
127
127
|
narrate(`run started · ${scriptPath()}`);
|
|
128
128
|
// Start streaming log entries to disk immediately — incremental writes survive
|
|
129
|
-
// hard kills where the final writeLog never runs.
|
|
130
|
-
|
|
129
|
+
// hard kills where the final writeLog never runs. Kept so finalize can await it
|
|
130
|
+
// *before* writeLog rewrites the same files: otherwise a late flush from this
|
|
131
|
+
// stream lands past writeLog's truncation and the OS zero-fills the gap (a
|
|
132
|
+
// multi-MB NUL run in run.log).
|
|
133
|
+
const streaming = streamLog(logs).catch(() => { });
|
|
131
134
|
const launch = (instance) => {
|
|
132
135
|
narrate(`launch ${instance.name}`);
|
|
133
136
|
// The action logs into the one shared store (entries carry their `source`;
|
|
@@ -184,6 +187,10 @@ export function run(...actions) {
|
|
|
184
187
|
process.off("uncaughtException", onUncaught);
|
|
185
188
|
process.off("unhandledRejection", onUnhandled);
|
|
186
189
|
narrate("finished");
|
|
190
|
+
// Wait for the incremental streamer to fully close its files, so its last
|
|
191
|
+
// flush can't land after writeLog truncates+rewrites them (which would leave
|
|
192
|
+
// a NUL gap). The log is already closed, so this resolves promptly.
|
|
193
|
+
await streaming;
|
|
187
194
|
const file = await writeLog(logs);
|
|
188
195
|
// Direct to the terminal — the file's already written, so this line isn't in it.
|
|
189
196
|
console.log(marker("run", `log → ${file}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|