@rpgm-tools/neo-angband-core 0.15.3 → 0.17.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/README.md +1 -1
- package/dist/log.d.ts +144 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +273 -0
- package/dist/log.js.map +1 -0
- package/dist/mod/save-blocks.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/src/log.ts +329 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ import { Rng, ENGINE_VERSION, PARITY_BASELINE } from "@rpgm-tools/neo-angband-co
|
|
|
32
32
|
const rng = new Rng(1234);
|
|
33
33
|
console.log(rng.damroll(3, 6), rng.damroll(3, 6), rng.damroll(3, 6)); // 9 13 8
|
|
34
34
|
|
|
35
|
-
console.log(ENGINE_VERSION, PARITY_BASELINE); // 0.
|
|
35
|
+
console.log(ENGINE_VERSION, PARITY_BASELINE); // 0.17.0 4.2.6
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Two entry points:
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A log a player can hand you, and the rule that decides how much is in it.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS IN `core` WHEN THE ENGINE NEVER CALLS IT. It is not a parity
|
|
5
|
+
* module and nothing in the C corresponds to it - the honest reason is that
|
|
6
|
+
* `core` is the only package both the renderer and the Electron main process
|
|
7
|
+
* already depend on, and the alternative was two implementations of the same
|
|
8
|
+
* format. This project has been bitten by that shape before: two copies of a
|
|
9
|
+
* check, and only one of them learns. So it lives here, behind the `./log`
|
|
10
|
+
* subpath rather than in the barrel, next to `./host` - which is the same kind
|
|
11
|
+
* of thing, infrastructure the port needs and the original did not have.
|
|
12
|
+
*
|
|
13
|
+
* It does NO I/O and holds NO sinks of its own. Writing a line to a file is the
|
|
14
|
+
* main process's job and drawing one on a terminal is the renderer's; this
|
|
15
|
+
* module owns the level rule, the record, the ring, and the text - the four
|
|
16
|
+
* things that have to be identical on both sides of the IPC boundary or a log
|
|
17
|
+
* file becomes two interleaved formats.
|
|
18
|
+
*
|
|
19
|
+
* THE RING IS THE POINT, not the file. A player who hits a bug is asked for a
|
|
20
|
+
* report, and the report is "the last N things that happened" - which has to
|
|
21
|
+
* exist in memory already, because the interesting part is over by the time
|
|
22
|
+
* anybody presses a key.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Four levels, and no `trace`.
|
|
26
|
+
*
|
|
27
|
+
* `debug` is already the level nobody ships; a fifth below it would only ever
|
|
28
|
+
* be reached by editing a constant, which is what a comment is for.
|
|
29
|
+
*/
|
|
30
|
+
export type LogLevel = "error" | "warn" | "info" | "debug";
|
|
31
|
+
/** Most severe first. Index is the rank, so the order here IS the ordering. */
|
|
32
|
+
export declare const LOG_LEVELS: readonly LogLevel[];
|
|
33
|
+
/** 0 for `error`, 3 for `debug`. -1 for anything that is not a level. */
|
|
34
|
+
export declare function logLevelRank(level: string): number;
|
|
35
|
+
export declare function isLogLevel(v: unknown): v is LogLevel;
|
|
36
|
+
/** Would a log running at `active` record something logged at `of`? */
|
|
37
|
+
export declare function logLevelAllows(active: LogLevel, of: LogLevel): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* How much this BUILD logs, decided by its own version rather than by a setting.
|
|
40
|
+
*
|
|
41
|
+
* A released build logs warnings and errors; a build somebody is testing logs
|
|
42
|
+
* what it is doing as well. The two states are already written down in the
|
|
43
|
+
* version string and nowhere else needs to agree:
|
|
44
|
+
*
|
|
45
|
+
* `0.16.1-edge.2` a per-commit build -> info
|
|
46
|
+
* `0.16.0` a 0.x pre-release -> info
|
|
47
|
+
* `1.2.3` a finished release -> warn
|
|
48
|
+
*
|
|
49
|
+
* DELIBERATELY NOT THE UPDATE CHANNEL. The channel is a preference about which
|
|
50
|
+
* builds to accept next, stored in localStorage and changeable at any time; a
|
|
51
|
+
* player who installed a beta and then picked `stable` is still running the
|
|
52
|
+
* beta, and the log should still say so. Asking the version means the answer
|
|
53
|
+
* cannot drift from the thing it describes.
|
|
54
|
+
*
|
|
55
|
+
* While the project is 0.x this returns `info` for everything, which is correct
|
|
56
|
+
* and temporary: `stable` selects nothing before 1.0.0 either. Both facts stop
|
|
57
|
+
* being true on the same day, by themselves - see `defaultChannel`, which this
|
|
58
|
+
* mirrors and which `update.test.ts` ties it to.
|
|
59
|
+
*/
|
|
60
|
+
export declare function defaultLogLevel(version: string): LogLevel;
|
|
61
|
+
/**
|
|
62
|
+
* One line of log, with its detail ALREADY rendered to text.
|
|
63
|
+
*
|
|
64
|
+
* Serialising at record time rather than at write time is what bounds the ring:
|
|
65
|
+
* a record holds a string, not a reference to a live game object that will have
|
|
66
|
+
* changed - or been mutated into a cycle - by the time anybody reads it. It also
|
|
67
|
+
* means a report cannot be the thing that throws while somebody is filing a bug.
|
|
68
|
+
*/
|
|
69
|
+
export interface LogRecord {
|
|
70
|
+
/** Epoch milliseconds. */
|
|
71
|
+
readonly at: number;
|
|
72
|
+
readonly level: LogLevel;
|
|
73
|
+
/** Where it came from: `update`, `mods`, `save`. Short and stable. */
|
|
74
|
+
readonly area: string;
|
|
75
|
+
readonly msg: string;
|
|
76
|
+
/** The extra detail, JSON-ish, or undefined when there was none. */
|
|
77
|
+
readonly data?: string;
|
|
78
|
+
}
|
|
79
|
+
/** Somewhere a line goes. Given the record and the text, so it need not reformat. */
|
|
80
|
+
export type LogSink = (rec: LogRecord, line: string) => void;
|
|
81
|
+
/** How many records the ring holds before the oldest is dropped. */
|
|
82
|
+
export declare const LOG_RING_DEFAULT = 2000;
|
|
83
|
+
/** Longest single message or detail string kept, in characters. */
|
|
84
|
+
export declare const LOG_FIELD_MAX = 2000;
|
|
85
|
+
/**
|
|
86
|
+
* JSON, but it cannot throw and it cannot run away.
|
|
87
|
+
*
|
|
88
|
+
* Everything a caller might hand this is in scope: a cycle (a game object
|
|
89
|
+
* pointing at the state that owns it), a BigInt (JSON.stringify throws on one),
|
|
90
|
+
* a 40,000-element array, a function, an Error (whose message and stack are the
|
|
91
|
+
* only interesting parts and neither is enumerable). A logger that throws while
|
|
92
|
+
* describing a failure replaces the failure with its own.
|
|
93
|
+
*/
|
|
94
|
+
export declare function describeValue(v: unknown, depth?: number): string;
|
|
95
|
+
/**
|
|
96
|
+
* One line of the log file.
|
|
97
|
+
*
|
|
98
|
+
* Fixed-width level so the areas line up when a human reads a thousand of them,
|
|
99
|
+
* and the detail after a ` | ` so the message can be found by eye without the
|
|
100
|
+
* JSON in the way.
|
|
101
|
+
*/
|
|
102
|
+
export declare function formatLogLine(rec: LogRecord): string;
|
|
103
|
+
/**
|
|
104
|
+
* A path with the user's home directory replaced by `~`.
|
|
105
|
+
*
|
|
106
|
+
* Called on the way into a REPORT, not on the way into the log file. The file
|
|
107
|
+
* lives on their machine and is theirs; the report is the thing they send to a
|
|
108
|
+
* stranger, and `C:\Users\firstname.lastname\...` is a real name in it. The
|
|
109
|
+
* comparison is case-insensitive because Windows paths are, and it is a plain
|
|
110
|
+
* substring so it catches the home directory wherever it appears in a line -
|
|
111
|
+
* inside a stack trace, inside a JSON blob - not only at the start.
|
|
112
|
+
*/
|
|
113
|
+
export declare function elideHome(text: string, home: string | undefined): string;
|
|
114
|
+
export interface LogOptions {
|
|
115
|
+
/** Starting level. Callers pass `defaultLogLevel(ENGINE_VERSION)`. */
|
|
116
|
+
readonly level: LogLevel;
|
|
117
|
+
/** Records held in memory. */
|
|
118
|
+
readonly capacity?: number;
|
|
119
|
+
/** Injected so a test can assert a timestamp instead of tolerating one. */
|
|
120
|
+
readonly now?: () => number;
|
|
121
|
+
}
|
|
122
|
+
/** What the rest of the app holds. */
|
|
123
|
+
export interface Log {
|
|
124
|
+
readonly level: LogLevel;
|
|
125
|
+
setLevel(level: LogLevel): void;
|
|
126
|
+
error(area: string, msg: string, data?: unknown): void;
|
|
127
|
+
warn(area: string, msg: string, data?: unknown): void;
|
|
128
|
+
info(area: string, msg: string, data?: unknown): void;
|
|
129
|
+
debug(area: string, msg: string, data?: unknown): void;
|
|
130
|
+
/** Add a destination. Returns the function that removes it again. */
|
|
131
|
+
addSink(sink: LogSink): () => void;
|
|
132
|
+
/** The ring, oldest first. */
|
|
133
|
+
recent(limit?: number): LogRecord[];
|
|
134
|
+
/**
|
|
135
|
+
* How many records the ring has thrown away.
|
|
136
|
+
*
|
|
137
|
+
* Reported on a report rather than kept quiet: "the last 2000 lines" and "the
|
|
138
|
+
* whole session" look identical in a text file, and the difference is whether
|
|
139
|
+
* the thing you are looking for could still be above the top.
|
|
140
|
+
*/
|
|
141
|
+
dropped(): number;
|
|
142
|
+
}
|
|
143
|
+
export declare function createLog(opts: LogOptions): Log;
|
|
144
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"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"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A log a player can hand you, and the rule that decides how much is in it.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS IN `core` WHEN THE ENGINE NEVER CALLS IT. It is not a parity
|
|
5
|
+
* module and nothing in the C corresponds to it - the honest reason is that
|
|
6
|
+
* `core` is the only package both the renderer and the Electron main process
|
|
7
|
+
* already depend on, and the alternative was two implementations of the same
|
|
8
|
+
* format. This project has been bitten by that shape before: two copies of a
|
|
9
|
+
* check, and only one of them learns. So it lives here, behind the `./log`
|
|
10
|
+
* subpath rather than in the barrel, next to `./host` - which is the same kind
|
|
11
|
+
* of thing, infrastructure the port needs and the original did not have.
|
|
12
|
+
*
|
|
13
|
+
* It does NO I/O and holds NO sinks of its own. Writing a line to a file is the
|
|
14
|
+
* main process's job and drawing one on a terminal is the renderer's; this
|
|
15
|
+
* module owns the level rule, the record, the ring, and the text - the four
|
|
16
|
+
* things that have to be identical on both sides of the IPC boundary or a log
|
|
17
|
+
* file becomes two interleaved formats.
|
|
18
|
+
*
|
|
19
|
+
* THE RING IS THE POINT, not the file. A player who hits a bug is asked for a
|
|
20
|
+
* report, and the report is "the last N things that happened" - which has to
|
|
21
|
+
* exist in memory already, because the interesting part is over by the time
|
|
22
|
+
* anybody presses a key.
|
|
23
|
+
*/
|
|
24
|
+
/** Most severe first. Index is the rank, so the order here IS the ordering. */
|
|
25
|
+
export const LOG_LEVELS = ["error", "warn", "info", "debug"];
|
|
26
|
+
/** 0 for `error`, 3 for `debug`. -1 for anything that is not a level. */
|
|
27
|
+
export function logLevelRank(level) {
|
|
28
|
+
return LOG_LEVELS.indexOf(level);
|
|
29
|
+
}
|
|
30
|
+
export function isLogLevel(v) {
|
|
31
|
+
return typeof v === "string" && logLevelRank(v) >= 0;
|
|
32
|
+
}
|
|
33
|
+
/** Would a log running at `active` record something logged at `of`? */
|
|
34
|
+
export function logLevelAllows(active, of) {
|
|
35
|
+
return logLevelRank(of) <= logLevelRank(active);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* How much this BUILD logs, decided by its own version rather than by a setting.
|
|
39
|
+
*
|
|
40
|
+
* A released build logs warnings and errors; a build somebody is testing logs
|
|
41
|
+
* what it is doing as well. The two states are already written down in the
|
|
42
|
+
* version string and nowhere else needs to agree:
|
|
43
|
+
*
|
|
44
|
+
* `0.16.1-edge.2` a per-commit build -> info
|
|
45
|
+
* `0.16.0` a 0.x pre-release -> info
|
|
46
|
+
* `1.2.3` a finished release -> warn
|
|
47
|
+
*
|
|
48
|
+
* DELIBERATELY NOT THE UPDATE CHANNEL. The channel is a preference about which
|
|
49
|
+
* builds to accept next, stored in localStorage and changeable at any time; a
|
|
50
|
+
* player who installed a beta and then picked `stable` is still running the
|
|
51
|
+
* beta, and the log should still say so. Asking the version means the answer
|
|
52
|
+
* cannot drift from the thing it describes.
|
|
53
|
+
*
|
|
54
|
+
* While the project is 0.x this returns `info` for everything, which is correct
|
|
55
|
+
* and temporary: `stable` selects nothing before 1.0.0 either. Both facts stop
|
|
56
|
+
* being true on the same day, by themselves - see `defaultChannel`, which this
|
|
57
|
+
* mirrors and which `update.test.ts` ties it to.
|
|
58
|
+
*/
|
|
59
|
+
export function defaultLogLevel(version) {
|
|
60
|
+
if (version.includes("-edge."))
|
|
61
|
+
return "info";
|
|
62
|
+
return /^0\./u.test(version) ? "info" : "warn";
|
|
63
|
+
}
|
|
64
|
+
/** How many records the ring holds before the oldest is dropped. */
|
|
65
|
+
export const LOG_RING_DEFAULT = 2000;
|
|
66
|
+
/** Longest single message or detail string kept, in characters. */
|
|
67
|
+
export const LOG_FIELD_MAX = 2000;
|
|
68
|
+
/**
|
|
69
|
+
* JSON, but it cannot throw and it cannot run away.
|
|
70
|
+
*
|
|
71
|
+
* Everything a caller might hand this is in scope: a cycle (a game object
|
|
72
|
+
* pointing at the state that owns it), a BigInt (JSON.stringify throws on one),
|
|
73
|
+
* a 40,000-element array, a function, an Error (whose message and stack are the
|
|
74
|
+
* only interesting parts and neither is enumerable). A logger that throws while
|
|
75
|
+
* describing a failure replaces the failure with its own.
|
|
76
|
+
*/
|
|
77
|
+
export function describeValue(v, depth = 4) {
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const walk = (x, left) => {
|
|
80
|
+
if (x === null || typeof x !== "object") {
|
|
81
|
+
if (typeof x === "bigint")
|
|
82
|
+
return `${x.toString()}n`;
|
|
83
|
+
if (typeof x === "function")
|
|
84
|
+
return `[function ${x.name || "anonymous"}]`;
|
|
85
|
+
if (typeof x === "symbol")
|
|
86
|
+
return x.toString();
|
|
87
|
+
if (typeof x === "string" && x.length > LOG_FIELD_MAX) {
|
|
88
|
+
return `${x.slice(0, LOG_FIELD_MAX)}...[${String(x.length)} chars]`;
|
|
89
|
+
}
|
|
90
|
+
return x;
|
|
91
|
+
}
|
|
92
|
+
if (seen.has(x))
|
|
93
|
+
return "[circular]";
|
|
94
|
+
if (left <= 0)
|
|
95
|
+
return "[...]";
|
|
96
|
+
if (x instanceof Error) {
|
|
97
|
+
/* name/message/stack are not enumerable, so the generic branch below
|
|
98
|
+
* would render every Error as `{}` - which is the single most useless
|
|
99
|
+
* thing a log can say about a failure. */
|
|
100
|
+
const out = { name: x.name, message: x.message };
|
|
101
|
+
if (typeof x.stack === "string")
|
|
102
|
+
out["stack"] = x.stack.split("\n").slice(0, 8).join("\n");
|
|
103
|
+
if (x.cause !== undefined)
|
|
104
|
+
out["cause"] = walk(x.cause, left - 1);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
seen.add(x);
|
|
108
|
+
try {
|
|
109
|
+
if (Array.isArray(x)) {
|
|
110
|
+
const head = x.slice(0, 50).map((e) => walk(e, left - 1));
|
|
111
|
+
return x.length > 50 ? [...head, `...[${String(x.length)} items]`] : head;
|
|
112
|
+
}
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const [k, val] of Object.entries(x)) {
|
|
115
|
+
out[k] = walk(val, left - 1);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
seen.delete(x);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
const text = JSON.stringify(walk(v, depth));
|
|
125
|
+
if (text === undefined)
|
|
126
|
+
return String(v);
|
|
127
|
+
return text.length > LOG_FIELD_MAX ? `${text.slice(0, LOG_FIELD_MAX)}...` : text;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* Belt and braces: a getter that throws is reachable from Object.entries. */
|
|
131
|
+
return "[undescribable]";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** `2026-08-02T13:06:09.123Z` - sortable, and the same in every timezone. */
|
|
135
|
+
function stamp(at) {
|
|
136
|
+
try {
|
|
137
|
+
return new Date(at).toISOString();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* A NaN or out-of-range timestamp must not cost the line it is attached to. */
|
|
141
|
+
return "????-??-??T??:??:??.???Z";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* One line of the log file.
|
|
146
|
+
*
|
|
147
|
+
* Fixed-width level so the areas line up when a human reads a thousand of them,
|
|
148
|
+
* and the detail after a ` | ` so the message can be found by eye without the
|
|
149
|
+
* JSON in the way.
|
|
150
|
+
*/
|
|
151
|
+
export function formatLogLine(rec) {
|
|
152
|
+
const head = `${stamp(rec.at)} ${rec.level.toUpperCase().padEnd(5)} [${rec.area}] ${rec.msg}`;
|
|
153
|
+
return rec.data === undefined ? head : `${head} | ${rec.data}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A path with the user's home directory replaced by `~`.
|
|
157
|
+
*
|
|
158
|
+
* Called on the way into a REPORT, not on the way into the log file. The file
|
|
159
|
+
* lives on their machine and is theirs; the report is the thing they send to a
|
|
160
|
+
* stranger, and `C:\Users\firstname.lastname\...` is a real name in it. The
|
|
161
|
+
* comparison is case-insensitive because Windows paths are, and it is a plain
|
|
162
|
+
* substring so it catches the home directory wherever it appears in a line -
|
|
163
|
+
* inside a stack trace, inside a JSON blob - not only at the start.
|
|
164
|
+
*/
|
|
165
|
+
export function elideHome(text, home) {
|
|
166
|
+
const h = (home ?? "").replace(/[\\/]+$/u, "");
|
|
167
|
+
if (h.length < 4)
|
|
168
|
+
return text;
|
|
169
|
+
/*
|
|
170
|
+
* THREE SPELLINGS OF THE SAME PATH, and the third is the one that matters.
|
|
171
|
+
*
|
|
172
|
+
* A path reaches the log through `describeValue`, which JSON-encodes it - so
|
|
173
|
+
* on Windows it arrives with its backslashes DOUBLED, and neither of the two
|
|
174
|
+
* obvious forms matches a character of it. Checking only the raw and URL forms
|
|
175
|
+
* left every path this project actually logs untouched. It was caught by a
|
|
176
|
+
* test that used a realistic log line rather than a hand-written path, which
|
|
177
|
+
* is the only reason it was caught at all.
|
|
178
|
+
*/
|
|
179
|
+
const forms = [
|
|
180
|
+
/* `{"path":"C:\\Users\\name\\..."}` - what describeValue produces. */
|
|
181
|
+
h.replace(/\\/gu, "\\\\"),
|
|
182
|
+
/* Raw, as node:path produces it. */
|
|
183
|
+
h,
|
|
184
|
+
/* A file:// URL in a stack trace, often in the same line as the above. */
|
|
185
|
+
h.replace(/\\/gu, "/"),
|
|
186
|
+
];
|
|
187
|
+
let out = text;
|
|
188
|
+
/* Longest first, so the doubled form is taken before the single one can eat
|
|
189
|
+
* its leading segment and leave the rest looking like an unrelated path. */
|
|
190
|
+
for (const form of [...new Set(forms)].sort((a, b) => b.length - a.length)) {
|
|
191
|
+
const pattern = new RegExp(form.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), "giu");
|
|
192
|
+
out = out.replace(pattern, "~");
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
export function createLog(opts) {
|
|
197
|
+
const capacity = Math.max(1, opts.capacity ?? LOG_RING_DEFAULT);
|
|
198
|
+
const now = opts.now ?? (() => Date.now());
|
|
199
|
+
const ring = new Array(capacity);
|
|
200
|
+
let head = 0;
|
|
201
|
+
let count = 0;
|
|
202
|
+
let lost = 0;
|
|
203
|
+
let level = opts.level;
|
|
204
|
+
const sinks = new Set();
|
|
205
|
+
const write = (lvl, area, msg, data) => {
|
|
206
|
+
if (!logLevelAllows(level, lvl))
|
|
207
|
+
return;
|
|
208
|
+
const rec = {
|
|
209
|
+
at: now(),
|
|
210
|
+
level: lvl,
|
|
211
|
+
area,
|
|
212
|
+
msg: msg.length > LOG_FIELD_MAX ? `${msg.slice(0, LOG_FIELD_MAX)}...` : msg,
|
|
213
|
+
...(data === undefined ? {} : { data: describeValue(data) }),
|
|
214
|
+
};
|
|
215
|
+
if (count === capacity)
|
|
216
|
+
lost++;
|
|
217
|
+
ring[head] = rec;
|
|
218
|
+
head = (head + 1) % capacity;
|
|
219
|
+
if (count < capacity)
|
|
220
|
+
count++;
|
|
221
|
+
const line = formatLogLine(rec);
|
|
222
|
+
for (const sink of sinks) {
|
|
223
|
+
try {
|
|
224
|
+
sink(rec, line);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* A sink that throws - a full disk, a closed IPC channel - must not
|
|
228
|
+
* take down the code that was only trying to say what it was doing,
|
|
229
|
+
* and must not stop the OTHER sinks from getting the line. */
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
get level() {
|
|
235
|
+
return level;
|
|
236
|
+
},
|
|
237
|
+
setLevel(next) {
|
|
238
|
+
level = next;
|
|
239
|
+
},
|
|
240
|
+
error: (a, m, d) => {
|
|
241
|
+
write("error", a, m, d);
|
|
242
|
+
},
|
|
243
|
+
warn: (a, m, d) => {
|
|
244
|
+
write("warn", a, m, d);
|
|
245
|
+
},
|
|
246
|
+
info: (a, m, d) => {
|
|
247
|
+
write("info", a, m, d);
|
|
248
|
+
},
|
|
249
|
+
debug: (a, m, d) => {
|
|
250
|
+
write("debug", a, m, d);
|
|
251
|
+
},
|
|
252
|
+
addSink(sink) {
|
|
253
|
+
sinks.add(sink);
|
|
254
|
+
return () => {
|
|
255
|
+
sinks.delete(sink);
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
recent(limit) {
|
|
259
|
+
const want = Math.max(0, Math.min(limit ?? count, count));
|
|
260
|
+
const out = [];
|
|
261
|
+
for (let i = count - want; i < count; i++) {
|
|
262
|
+
const rec = ring[(head - count + i + capacity * 2) % capacity];
|
|
263
|
+
if (rec)
|
|
264
|
+
out.push(rec);
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
},
|
|
268
|
+
dropped() {
|
|
269
|
+
return lost;
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"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"}
|
|
@@ -128,7 +128,7 @@ export type OrphanStore = Record<string, OrphanEntry[]>;
|
|
|
128
128
|
* core pack outright, and versionMap's versions are only ever used to key
|
|
129
129
|
* quarantined MOD entities. An existing save keeps whatever it recorded.
|
|
130
130
|
*/
|
|
131
|
-
export declare const CORE_PACK_VERSION = "0.
|
|
131
|
+
export declare const CORE_PACK_VERSION = "0.17.0";
|
|
132
132
|
/** A core-only manifest: the base game with no mods, deterministic. */
|
|
133
133
|
export declare function coreOnlyManifest(): SaveManifest;
|
|
134
134
|
/**
|
package/dist/version.d.ts
CHANGED
|
@@ -21,5 +21,5 @@ export declare const PARITY_BASELINE = "4.2.6";
|
|
|
21
21
|
* released tag is iterated takes a MINOR bump, because a published tag is pinned
|
|
22
22
|
* by digest in a catalogue and must never be moved.
|
|
23
23
|
*/
|
|
24
|
-
export declare const ENGINE_VERSION = "0.
|
|
24
|
+
export declare const ENGINE_VERSION = "0.17.0";
|
|
25
25
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -21,5 +21,5 @@ export const PARITY_BASELINE = "4.2.6";
|
|
|
21
21
|
* released tag is iterated takes a MINOR bump, because a published tag is pinned
|
|
22
22
|
* by digest in a catalogue and must never be moved.
|
|
23
23
|
*/
|
|
24
|
-
export const ENGINE_VERSION = "0.
|
|
24
|
+
export const ENGINE_VERSION = "0.17.0";
|
|
25
25
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgm-tools/neo-angband-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Headless Neo Angband game engine: rules, world, entities, effects, generation",
|
|
5
5
|
"license": "GPL-2.0-only",
|
|
6
6
|
"author": "neostryder (RPGM Tools)",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"types": "./dist/host/index.d.ts",
|
|
35
35
|
"default": "./dist/host/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./log": {
|
|
38
|
+
"types": "./dist/log.d.ts",
|
|
39
|
+
"default": "./dist/log.js"
|
|
40
|
+
},
|
|
37
41
|
"./package.json": "./package.json"
|
|
38
42
|
},
|
|
39
43
|
"//files": [
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A log a player can hand you, and the rule that decides how much is in it.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS IN `core` WHEN THE ENGINE NEVER CALLS IT. It is not a parity
|
|
5
|
+
* module and nothing in the C corresponds to it - the honest reason is that
|
|
6
|
+
* `core` is the only package both the renderer and the Electron main process
|
|
7
|
+
* already depend on, and the alternative was two implementations of the same
|
|
8
|
+
* format. This project has been bitten by that shape before: two copies of a
|
|
9
|
+
* check, and only one of them learns. So it lives here, behind the `./log`
|
|
10
|
+
* subpath rather than in the barrel, next to `./host` - which is the same kind
|
|
11
|
+
* of thing, infrastructure the port needs and the original did not have.
|
|
12
|
+
*
|
|
13
|
+
* It does NO I/O and holds NO sinks of its own. Writing a line to a file is the
|
|
14
|
+
* main process's job and drawing one on a terminal is the renderer's; this
|
|
15
|
+
* module owns the level rule, the record, the ring, and the text - the four
|
|
16
|
+
* things that have to be identical on both sides of the IPC boundary or a log
|
|
17
|
+
* file becomes two interleaved formats.
|
|
18
|
+
*
|
|
19
|
+
* THE RING IS THE POINT, not the file. A player who hits a bug is asked for a
|
|
20
|
+
* report, and the report is "the last N things that happened" - which has to
|
|
21
|
+
* exist in memory already, because the interesting part is over by the time
|
|
22
|
+
* anybody presses a key.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Four levels, and no `trace`.
|
|
27
|
+
*
|
|
28
|
+
* `debug` is already the level nobody ships; a fifth below it would only ever
|
|
29
|
+
* be reached by editing a constant, which is what a comment is for.
|
|
30
|
+
*/
|
|
31
|
+
export type LogLevel = "error" | "warn" | "info" | "debug";
|
|
32
|
+
|
|
33
|
+
/** Most severe first. Index is the rank, so the order here IS the ordering. */
|
|
34
|
+
export const LOG_LEVELS: readonly LogLevel[] = ["error", "warn", "info", "debug"];
|
|
35
|
+
|
|
36
|
+
/** 0 for `error`, 3 for `debug`. -1 for anything that is not a level. */
|
|
37
|
+
export function logLevelRank(level: string): number {
|
|
38
|
+
return (LOG_LEVELS as readonly string[]).indexOf(level);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isLogLevel(v: unknown): v is LogLevel {
|
|
42
|
+
return typeof v === "string" && logLevelRank(v) >= 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Would a log running at `active` record something logged at `of`? */
|
|
46
|
+
export function logLevelAllows(active: LogLevel, of: LogLevel): boolean {
|
|
47
|
+
return logLevelRank(of) <= logLevelRank(active);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* How much this BUILD logs, decided by its own version rather than by a setting.
|
|
52
|
+
*
|
|
53
|
+
* A released build logs warnings and errors; a build somebody is testing logs
|
|
54
|
+
* what it is doing as well. The two states are already written down in the
|
|
55
|
+
* version string and nowhere else needs to agree:
|
|
56
|
+
*
|
|
57
|
+
* `0.16.1-edge.2` a per-commit build -> info
|
|
58
|
+
* `0.16.0` a 0.x pre-release -> info
|
|
59
|
+
* `1.2.3` a finished release -> warn
|
|
60
|
+
*
|
|
61
|
+
* DELIBERATELY NOT THE UPDATE CHANNEL. The channel is a preference about which
|
|
62
|
+
* builds to accept next, stored in localStorage and changeable at any time; a
|
|
63
|
+
* player who installed a beta and then picked `stable` is still running the
|
|
64
|
+
* beta, and the log should still say so. Asking the version means the answer
|
|
65
|
+
* cannot drift from the thing it describes.
|
|
66
|
+
*
|
|
67
|
+
* While the project is 0.x this returns `info` for everything, which is correct
|
|
68
|
+
* and temporary: `stable` selects nothing before 1.0.0 either. Both facts stop
|
|
69
|
+
* being true on the same day, by themselves - see `defaultChannel`, which this
|
|
70
|
+
* mirrors and which `update.test.ts` ties it to.
|
|
71
|
+
*/
|
|
72
|
+
export function defaultLogLevel(version: string): LogLevel {
|
|
73
|
+
if (version.includes("-edge.")) return "info";
|
|
74
|
+
return /^0\./u.test(version) ? "info" : "warn";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One line of log, with its detail ALREADY rendered to text.
|
|
79
|
+
*
|
|
80
|
+
* Serialising at record time rather than at write time is what bounds the ring:
|
|
81
|
+
* a record holds a string, not a reference to a live game object that will have
|
|
82
|
+
* changed - or been mutated into a cycle - by the time anybody reads it. It also
|
|
83
|
+
* means a report cannot be the thing that throws while somebody is filing a bug.
|
|
84
|
+
*/
|
|
85
|
+
export interface LogRecord {
|
|
86
|
+
/** Epoch milliseconds. */
|
|
87
|
+
readonly at: number;
|
|
88
|
+
readonly level: LogLevel;
|
|
89
|
+
/** Where it came from: `update`, `mods`, `save`. Short and stable. */
|
|
90
|
+
readonly area: string;
|
|
91
|
+
readonly msg: string;
|
|
92
|
+
/** The extra detail, JSON-ish, or undefined when there was none. */
|
|
93
|
+
readonly data?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Somewhere a line goes. Given the record and the text, so it need not reformat. */
|
|
97
|
+
export type LogSink = (rec: LogRecord, line: string) => void;
|
|
98
|
+
|
|
99
|
+
/** How many records the ring holds before the oldest is dropped. */
|
|
100
|
+
export const LOG_RING_DEFAULT = 2000;
|
|
101
|
+
|
|
102
|
+
/** Longest single message or detail string kept, in characters. */
|
|
103
|
+
export const LOG_FIELD_MAX = 2000;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* JSON, but it cannot throw and it cannot run away.
|
|
107
|
+
*
|
|
108
|
+
* Everything a caller might hand this is in scope: a cycle (a game object
|
|
109
|
+
* pointing at the state that owns it), a BigInt (JSON.stringify throws on one),
|
|
110
|
+
* a 40,000-element array, a function, an Error (whose message and stack are the
|
|
111
|
+
* only interesting parts and neither is enumerable). A logger that throws while
|
|
112
|
+
* describing a failure replaces the failure with its own.
|
|
113
|
+
*/
|
|
114
|
+
export function describeValue(v: unknown, depth = 4): string {
|
|
115
|
+
const seen = new Set<unknown>();
|
|
116
|
+
const walk = (x: unknown, left: number): unknown => {
|
|
117
|
+
if (x === null || typeof x !== "object") {
|
|
118
|
+
if (typeof x === "bigint") return `${x.toString()}n`;
|
|
119
|
+
if (typeof x === "function") return `[function ${x.name || "anonymous"}]`;
|
|
120
|
+
if (typeof x === "symbol") return x.toString();
|
|
121
|
+
if (typeof x === "string" && x.length > LOG_FIELD_MAX) {
|
|
122
|
+
return `${x.slice(0, LOG_FIELD_MAX)}...[${String(x.length)} chars]`;
|
|
123
|
+
}
|
|
124
|
+
return x;
|
|
125
|
+
}
|
|
126
|
+
if (seen.has(x)) return "[circular]";
|
|
127
|
+
if (left <= 0) return "[...]";
|
|
128
|
+
if (x instanceof Error) {
|
|
129
|
+
/* name/message/stack are not enumerable, so the generic branch below
|
|
130
|
+
* would render every Error as `{}` - which is the single most useless
|
|
131
|
+
* thing a log can say about a failure. */
|
|
132
|
+
const out: Record<string, unknown> = { name: x.name, message: x.message };
|
|
133
|
+
if (typeof x.stack === "string") out["stack"] = x.stack.split("\n").slice(0, 8).join("\n");
|
|
134
|
+
if (x.cause !== undefined) out["cause"] = walk(x.cause, left - 1);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
seen.add(x);
|
|
138
|
+
try {
|
|
139
|
+
if (Array.isArray(x)) {
|
|
140
|
+
const head = x.slice(0, 50).map((e) => walk(e, left - 1));
|
|
141
|
+
return x.length > 50 ? [...head, `...[${String(x.length)} items]`] : head;
|
|
142
|
+
}
|
|
143
|
+
const out: Record<string, unknown> = {};
|
|
144
|
+
for (const [k, val] of Object.entries(x as Record<string, unknown>)) {
|
|
145
|
+
out[k] = walk(val, left - 1);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
} finally {
|
|
149
|
+
seen.delete(x);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
const text = JSON.stringify(walk(v, depth));
|
|
154
|
+
if (text === undefined) return String(v);
|
|
155
|
+
return text.length > LOG_FIELD_MAX ? `${text.slice(0, LOG_FIELD_MAX)}...` : text;
|
|
156
|
+
} catch {
|
|
157
|
+
/* Belt and braces: a getter that throws is reachable from Object.entries. */
|
|
158
|
+
return "[undescribable]";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `2026-08-02T13:06:09.123Z` - sortable, and the same in every timezone. */
|
|
163
|
+
function stamp(at: number): string {
|
|
164
|
+
try {
|
|
165
|
+
return new Date(at).toISOString();
|
|
166
|
+
} catch {
|
|
167
|
+
/* A NaN or out-of-range timestamp must not cost the line it is attached to. */
|
|
168
|
+
return "????-??-??T??:??:??.???Z";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One line of the log file.
|
|
174
|
+
*
|
|
175
|
+
* Fixed-width level so the areas line up when a human reads a thousand of them,
|
|
176
|
+
* and the detail after a ` | ` so the message can be found by eye without the
|
|
177
|
+
* JSON in the way.
|
|
178
|
+
*/
|
|
179
|
+
export function formatLogLine(rec: LogRecord): string {
|
|
180
|
+
const head = `${stamp(rec.at)} ${rec.level.toUpperCase().padEnd(5)} [${rec.area}] ${rec.msg}`;
|
|
181
|
+
return rec.data === undefined ? head : `${head} | ${rec.data}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A path with the user's home directory replaced by `~`.
|
|
186
|
+
*
|
|
187
|
+
* Called on the way into a REPORT, not on the way into the log file. The file
|
|
188
|
+
* lives on their machine and is theirs; the report is the thing they send to a
|
|
189
|
+
* stranger, and `C:\Users\firstname.lastname\...` is a real name in it. The
|
|
190
|
+
* comparison is case-insensitive because Windows paths are, and it is a plain
|
|
191
|
+
* substring so it catches the home directory wherever it appears in a line -
|
|
192
|
+
* inside a stack trace, inside a JSON blob - not only at the start.
|
|
193
|
+
*/
|
|
194
|
+
export function elideHome(text: string, home: string | undefined): string {
|
|
195
|
+
const h = (home ?? "").replace(/[\\/]+$/u, "");
|
|
196
|
+
if (h.length < 4) return text;
|
|
197
|
+
/*
|
|
198
|
+
* THREE SPELLINGS OF THE SAME PATH, and the third is the one that matters.
|
|
199
|
+
*
|
|
200
|
+
* A path reaches the log through `describeValue`, which JSON-encodes it - so
|
|
201
|
+
* on Windows it arrives with its backslashes DOUBLED, and neither of the two
|
|
202
|
+
* obvious forms matches a character of it. Checking only the raw and URL forms
|
|
203
|
+
* left every path this project actually logs untouched. It was caught by a
|
|
204
|
+
* test that used a realistic log line rather than a hand-written path, which
|
|
205
|
+
* is the only reason it was caught at all.
|
|
206
|
+
*/
|
|
207
|
+
const forms = [
|
|
208
|
+
/* `{"path":"C:\\Users\\name\\..."}` - what describeValue produces. */
|
|
209
|
+
h.replace(/\\/gu, "\\\\"),
|
|
210
|
+
/* Raw, as node:path produces it. */
|
|
211
|
+
h,
|
|
212
|
+
/* A file:// URL in a stack trace, often in the same line as the above. */
|
|
213
|
+
h.replace(/\\/gu, "/"),
|
|
214
|
+
];
|
|
215
|
+
let out = text;
|
|
216
|
+
/* Longest first, so the doubled form is taken before the single one can eat
|
|
217
|
+
* its leading segment and leave the rest looking like an unrelated path. */
|
|
218
|
+
for (const form of [...new Set(forms)].sort((a, b) => b.length - a.length)) {
|
|
219
|
+
const pattern = new RegExp(form.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), "giu");
|
|
220
|
+
out = out.replace(pattern, "~");
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface LogOptions {
|
|
226
|
+
/** Starting level. Callers pass `defaultLogLevel(ENGINE_VERSION)`. */
|
|
227
|
+
readonly level: LogLevel;
|
|
228
|
+
/** Records held in memory. */
|
|
229
|
+
readonly capacity?: number;
|
|
230
|
+
/** Injected so a test can assert a timestamp instead of tolerating one. */
|
|
231
|
+
readonly now?: () => number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** What the rest of the app holds. */
|
|
235
|
+
export interface Log {
|
|
236
|
+
readonly level: LogLevel;
|
|
237
|
+
setLevel(level: LogLevel): void;
|
|
238
|
+
error(area: string, msg: string, data?: unknown): void;
|
|
239
|
+
warn(area: string, msg: string, data?: unknown): void;
|
|
240
|
+
info(area: string, msg: string, data?: unknown): void;
|
|
241
|
+
debug(area: string, msg: string, data?: unknown): void;
|
|
242
|
+
/** Add a destination. Returns the function that removes it again. */
|
|
243
|
+
addSink(sink: LogSink): () => void;
|
|
244
|
+
/** The ring, oldest first. */
|
|
245
|
+
recent(limit?: number): LogRecord[];
|
|
246
|
+
/**
|
|
247
|
+
* How many records the ring has thrown away.
|
|
248
|
+
*
|
|
249
|
+
* Reported on a report rather than kept quiet: "the last 2000 lines" and "the
|
|
250
|
+
* whole session" look identical in a text file, and the difference is whether
|
|
251
|
+
* the thing you are looking for could still be above the top.
|
|
252
|
+
*/
|
|
253
|
+
dropped(): number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function createLog(opts: LogOptions): Log {
|
|
257
|
+
const capacity = Math.max(1, opts.capacity ?? LOG_RING_DEFAULT);
|
|
258
|
+
const now = opts.now ?? ((): number => Date.now());
|
|
259
|
+
const ring: (LogRecord | undefined)[] = new Array<LogRecord | undefined>(capacity);
|
|
260
|
+
let head = 0;
|
|
261
|
+
let count = 0;
|
|
262
|
+
let lost = 0;
|
|
263
|
+
let level = opts.level;
|
|
264
|
+
const sinks = new Set<LogSink>();
|
|
265
|
+
|
|
266
|
+
const write = (lvl: LogLevel, area: string, msg: string, data: unknown): void => {
|
|
267
|
+
if (!logLevelAllows(level, lvl)) return;
|
|
268
|
+
const rec: LogRecord = {
|
|
269
|
+
at: now(),
|
|
270
|
+
level: lvl,
|
|
271
|
+
area,
|
|
272
|
+
msg: msg.length > LOG_FIELD_MAX ? `${msg.slice(0, LOG_FIELD_MAX)}...` : msg,
|
|
273
|
+
...(data === undefined ? {} : { data: describeValue(data) }),
|
|
274
|
+
};
|
|
275
|
+
if (count === capacity) lost++;
|
|
276
|
+
ring[head] = rec;
|
|
277
|
+
head = (head + 1) % capacity;
|
|
278
|
+
if (count < capacity) count++;
|
|
279
|
+
const line = formatLogLine(rec);
|
|
280
|
+
for (const sink of sinks) {
|
|
281
|
+
try {
|
|
282
|
+
sink(rec, line);
|
|
283
|
+
} catch {
|
|
284
|
+
/* A sink that throws - a full disk, a closed IPC channel - must not
|
|
285
|
+
* take down the code that was only trying to say what it was doing,
|
|
286
|
+
* and must not stop the OTHER sinks from getting the line. */
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
get level(): LogLevel {
|
|
293
|
+
return level;
|
|
294
|
+
},
|
|
295
|
+
setLevel(next: LogLevel): void {
|
|
296
|
+
level = next;
|
|
297
|
+
},
|
|
298
|
+
error: (a, m, d?) => {
|
|
299
|
+
write("error", a, m, d);
|
|
300
|
+
},
|
|
301
|
+
warn: (a, m, d?) => {
|
|
302
|
+
write("warn", a, m, d);
|
|
303
|
+
},
|
|
304
|
+
info: (a, m, d?) => {
|
|
305
|
+
write("info", a, m, d);
|
|
306
|
+
},
|
|
307
|
+
debug: (a, m, d?) => {
|
|
308
|
+
write("debug", a, m, d);
|
|
309
|
+
},
|
|
310
|
+
addSink(sink: LogSink): () => void {
|
|
311
|
+
sinks.add(sink);
|
|
312
|
+
return () => {
|
|
313
|
+
sinks.delete(sink);
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
recent(limit?: number): LogRecord[] {
|
|
317
|
+
const want = Math.max(0, Math.min(limit ?? count, count));
|
|
318
|
+
const out: LogRecord[] = [];
|
|
319
|
+
for (let i = count - want; i < count; i++) {
|
|
320
|
+
const rec = ring[(head - count + i + capacity * 2) % capacity];
|
|
321
|
+
if (rec) out.push(rec);
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
},
|
|
325
|
+
dropped(): number {
|
|
326
|
+
return lost;
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -23,4 +23,4 @@ export const PARITY_BASELINE = "4.2.6";
|
|
|
23
23
|
* released tag is iterated takes a MINOR bump, because a published tag is pinned
|
|
24
24
|
* by digest in a catalogue and must never be moved.
|
|
25
25
|
*/
|
|
26
|
-
export const ENGINE_VERSION = "0.
|
|
26
|
+
export const ENGINE_VERSION = "0.17.0";
|