@volter/world-core 2.0.19 → 2.0.20
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/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/serve.d.ts +15 -5
- package/dist/src/serve.js +69 -28
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/serve.ts +74 -31
package/dist/src/index.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export { SqlWorldStore } from './world-store-sql.js';
|
|
|
33
33
|
export type { SqlExec } from './world-store-sql.js';
|
|
34
34
|
export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, } from './blob-store.js';
|
|
35
35
|
export type { BlobStore } from './blob-store.js';
|
|
36
|
-
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, journalTwinRequest, twinRequestCredentials, readTwinRequestJournal, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from './serve.js';
|
|
36
|
+
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, journalTwinRequest, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from './serve.js';
|
|
37
37
|
export type { AtomicTwinWriteDecision, TwinRequestJournalEntry, TwinResource, TwinWriteInput, TwinWriteResult, } from './serve.js';
|
|
38
38
|
export { createTwinProxy } from './proxy.js';
|
|
39
39
|
export type { TwinProxy, TwinProxyOptions, VendorRoute } from './proxy.js';
|
package/dist/src/index.js
CHANGED
|
@@ -55,7 +55,7 @@ export { SqlWorldStore } from "./world-store-sql.js";
|
|
|
55
55
|
// The blob seam (runtime contract R11): byte storage behind byte-carrying handlers, so a
|
|
56
56
|
// serverless namespace puts bytes in object storage while local worlds keep today's layout.
|
|
57
57
|
export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, } from "./blob-store.js";
|
|
58
|
-
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, journalTwinRequest, twinRequestCredentials, readTwinRequestJournal, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
|
|
58
|
+
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, journalTwinRequest, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
|
|
59
59
|
export { createTwinProxy } from "./proxy.js";
|
|
60
60
|
export { forkTwin, isFork, readForkMeta, } from "./fork.js";
|
|
61
61
|
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from "./actions.js";
|
package/dist/src/serve.d.ts
CHANGED
|
@@ -24,9 +24,18 @@ export type TwinRequestJournalEntry = {
|
|
|
24
24
|
ms?: number;
|
|
25
25
|
/** credential-looking headers/query params that arrived, named + fingerprinted, never quoted. */
|
|
26
26
|
credentials?: TwinCredentialShape[];
|
|
27
|
+
/** `request`: written BEFORE the request is served (an audit device's request entry, so a request whose
|
|
28
|
+
* audit cannot be written is refused); its outcome is the entry without a phase written after. */
|
|
29
|
+
phase?: 'request';
|
|
27
30
|
};
|
|
28
|
-
/** The journal read back: every entry kept for a service, oldest first (an empty list when the journal is switched off or empty).
|
|
29
|
-
|
|
31
|
+
/** The journal read back: every entry kept for a service, oldest first (an empty list when the journal is switched off or empty).
|
|
32
|
+
* With `since` (epoch ms), segments that ended before it are not read. */
|
|
33
|
+
export declare function readTwinRequestJournal(service: string, root?: string, options?: {
|
|
34
|
+
since?: number;
|
|
35
|
+
}): TwinRequestJournalEntry[];
|
|
36
|
+
/** Journal lines this process failed to keep, since it started, for one service's journal under `root`
|
|
37
|
+
* (a World's request report sums its own twins and links). */
|
|
38
|
+
export declare function twinRequestJournalFailures(service: string, root?: string): number;
|
|
30
39
|
/** Non-reversible, stable fingerprint. 64 bits of sha256 — enough that two distinct credentials
|
|
31
40
|
* never collide in a journal, far too little to walk back to a real key. */
|
|
32
41
|
export declare function credentialFingerprint(value: string): string;
|
|
@@ -41,10 +50,11 @@ export declare function twinRequestJournalEnabled(env?: Record<string, string |
|
|
|
41
50
|
/** Where a service's request journal lives: beside its `actions.jsonl`. */
|
|
42
51
|
export declare function twinRequestJournalPath(service: string, root?: string): string;
|
|
43
52
|
/**
|
|
44
|
-
* Append one
|
|
45
|
-
*
|
|
53
|
+
* Append one request to the journal — a no-op only when VOLTER_TWIN_REQUEST_JOURNAL=0. Never throws;
|
|
54
|
+
* answers whether the line was kept (true when the journal is switched off: no audit was chosen), so a
|
|
55
|
+
* caller that must not act unaudited (a link) can refuse.
|
|
46
56
|
*/
|
|
47
|
-
export declare function journalTwinRequest(service: string, entry: TwinRequestJournalEntry, root?: string):
|
|
57
|
+
export declare function journalTwinRequest(service: string, entry: TwinRequestJournalEntry, root?: string): boolean;
|
|
48
58
|
export declare const TWIN_JOURNAL_IDENTITY: unique symbol;
|
|
49
59
|
export type TwinJournalIdentity = {
|
|
50
60
|
service: string;
|
package/dist/src/serve.js
CHANGED
|
@@ -18,27 +18,60 @@ import { hashFieldValue } from "./hash.js";
|
|
|
18
18
|
import { appendActionIfAbsent, appendActionOccurrence, decideAndAppendAction, projectResources } from "./actions.js";
|
|
19
19
|
import { observeWorldPaths, worldPaths } from "./storage.js";
|
|
20
20
|
import { getActiveWorldStore } from "./world-store.js";
|
|
21
|
-
/** The journal
|
|
22
|
-
|
|
21
|
+
/** The journal's closed segments, oldest first: `requests.jsonl.<n>` beside the live file. */
|
|
22
|
+
function journalSegments(path) {
|
|
23
|
+
const base = path.slice(path.lastIndexOf('/') + 1);
|
|
24
|
+
return getActiveWorldStore().list(dirname(path))
|
|
25
|
+
.map((name) => (name.startsWith(`${base}.`) ? Number(name.slice(base.length + 1)) : NaN))
|
|
26
|
+
.filter((n) => Number.isInteger(n) && n > 0)
|
|
27
|
+
.sort((a, b) => a - b)
|
|
28
|
+
.map((n) => `${path}.${n}`);
|
|
29
|
+
}
|
|
30
|
+
/** The journal read back: every entry kept for a service, oldest first (an empty list when the journal is switched off or empty).
|
|
31
|
+
* With `since` (epoch ms), segments that ended before it are not read. */
|
|
32
|
+
export function readTwinRequestJournal(service, root, options = {}) {
|
|
23
33
|
const path = twinRequestJournalPath(service, root);
|
|
24
|
-
const
|
|
25
|
-
for (const file of [`${path}.1`, path]) {
|
|
34
|
+
const parse = (file) => {
|
|
26
35
|
const text = getActiveWorldStore().read(file);
|
|
27
36
|
if (!text)
|
|
28
|
-
|
|
37
|
+
return [];
|
|
38
|
+
const rows = [];
|
|
29
39
|
for (const line of text.split('\n')) {
|
|
30
40
|
if (!line)
|
|
31
41
|
continue;
|
|
32
42
|
try {
|
|
33
|
-
|
|
43
|
+
rows.push(JSON.parse(line));
|
|
34
44
|
}
|
|
35
|
-
catch { /* a torn line
|
|
45
|
+
catch { /* a torn line */ }
|
|
36
46
|
}
|
|
47
|
+
return rows;
|
|
48
|
+
};
|
|
49
|
+
const kept = [parse(path)];
|
|
50
|
+
// newest first, stopping at the first segment that ended before `since`
|
|
51
|
+
for (const file of journalSegments(path).reverse()) {
|
|
52
|
+
const rows = parse(file);
|
|
53
|
+
const last = rows[rows.length - 1]?.at;
|
|
54
|
+
if (options.since !== undefined && last && Date.parse(last) < options.since)
|
|
55
|
+
break;
|
|
56
|
+
kept.unshift(rows);
|
|
57
|
+
}
|
|
58
|
+
return kept.flat();
|
|
59
|
+
}
|
|
60
|
+
/** Lines not kept, per journal: keyed by the journal's own path, so a process serving many Worlds
|
|
61
|
+
* (a host; an isolate holding several) answers for each World alone. */
|
|
62
|
+
const journalFailures = new Map();
|
|
63
|
+
/** Journal lines this process failed to keep, since it started, for one service's journal under `root`
|
|
64
|
+
* (a World's request report sums its own twins and links). */
|
|
65
|
+
export function twinRequestJournalFailures(service, root) {
|
|
66
|
+
try {
|
|
67
|
+
return journalFailures.get(twinRequestJournalPath(service, root)) ?? 0;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 0;
|
|
37
71
|
}
|
|
38
|
-
return out;
|
|
39
72
|
}
|
|
40
|
-
/** ~1 MB
|
|
41
|
-
*
|
|
73
|
+
/** ~1 MB per segment: tens of thousands of entries (a shape line is ~80 bytes), and a report over a
|
|
74
|
+
* recent period reads only the segments that reach into it. */
|
|
42
75
|
const REQUEST_JOURNAL_MAX_BYTES = 1_000_000;
|
|
43
76
|
/**
|
|
44
77
|
* Does this header/param NAME look like it carries a credential? Deliberately a generic word
|
|
@@ -112,34 +145,42 @@ export function twinRequestJournalPath(service, root) {
|
|
|
112
145
|
return join(dirname(worldPaths(service, root).events), 'requests.jsonl');
|
|
113
146
|
}
|
|
114
147
|
/**
|
|
115
|
-
* Append one
|
|
116
|
-
*
|
|
148
|
+
* Append one request to the journal — a no-op only when VOLTER_TWIN_REQUEST_JOURNAL=0. Never throws;
|
|
149
|
+
* answers whether the line was kept (true when the journal is switched off: no audit was chosen), so a
|
|
150
|
+
* caller that must not act unaudited (a link) can refuse.
|
|
117
151
|
*/
|
|
118
152
|
export function journalTwinRequest(service, entry, root) {
|
|
119
153
|
if (!twinRequestJournalEnabled())
|
|
120
|
-
return;
|
|
154
|
+
return true;
|
|
155
|
+
const line = JSON.stringify({
|
|
156
|
+
at: entry.at ?? new Date().toISOString(),
|
|
157
|
+
...(entry.phase ? { phase: entry.phase } : {}),
|
|
158
|
+
method: entry.method.toUpperCase(),
|
|
159
|
+
path: entry.path.split('?')[0], // the query STRING never lands; its credential params do, named + fingerprinted
|
|
160
|
+
status: entry.status,
|
|
161
|
+
...(typeof entry.ms === 'number' ? { ms: entry.ms } : {}),
|
|
162
|
+
...(entry.credentials?.length ? { credentials: entry.credentials } : {}),
|
|
163
|
+
});
|
|
164
|
+
let path;
|
|
121
165
|
try {
|
|
122
166
|
const store = getActiveWorldStore();
|
|
123
|
-
|
|
167
|
+
path = twinRequestJournalPath(service, root);
|
|
124
168
|
store.mkdir(dirname(path));
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
169
|
+
if ((store.stat(path)?.size ?? 0) >= REQUEST_JOURNAL_MAX_BYTES) {
|
|
170
|
+
// the live file closes into the next segment; no segment is ever overwritten or removed (a crash between
|
|
171
|
+
// the two writes leaves the live lines in both: repeated in the next segment, never lost)
|
|
172
|
+
const next = Number(journalSegments(path).pop()?.slice(path.length + 1) ?? 0) + 1;
|
|
173
|
+
store.writeAtomic(`${path}.${next}`, store.read(path) ?? '');
|
|
129
174
|
store.write(path, '');
|
|
130
175
|
}
|
|
131
|
-
const line = JSON.stringify({
|
|
132
|
-
at: entry.at ?? new Date().toISOString(),
|
|
133
|
-
method: entry.method.toUpperCase(),
|
|
134
|
-
path: entry.path.split('?')[0], // the query STRING never lands; its credential params do, named + fingerprinted
|
|
135
|
-
status: entry.status,
|
|
136
|
-
...(typeof entry.ms === 'number' ? { ms: entry.ms } : {}),
|
|
137
|
-
...(entry.credentials?.length ? { credentials: entry.credentials } : {}),
|
|
138
|
-
});
|
|
139
176
|
store.append(path, `${line}\n`);
|
|
177
|
+
return true;
|
|
140
178
|
}
|
|
141
|
-
catch {
|
|
142
|
-
|
|
179
|
+
catch (error) {
|
|
180
|
+
const key = path ?? `${root ?? ''}/${service}`;
|
|
181
|
+
journalFailures.set(key, (journalFailures.get(key) ?? 0) + 1);
|
|
182
|
+
console.error(`[request-journal] ${service}: a line was not kept: ${error.message}`);
|
|
183
|
+
return false;
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
// ── how a wrapped `Bun.serve` learns which twin it is ──────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/index.ts
CHANGED
package/src/serve.ts
CHANGED
|
@@ -25,9 +25,11 @@ import { getActiveWorldStore } from './world-store.ts';
|
|
|
25
25
|
// `actions.jsonl` records MUTATIONS only, so a twin's READS are invisible to `volter-world tail`
|
|
26
26
|
// — an empty-catalog GET that 404s leaves no trace, and "did the app even ask?" is unanswerable.
|
|
27
27
|
// The journal is the minimal honest answer: a per-twin `requests.jsonl` beside `actions.jsonl`, one
|
|
28
|
-
// line per served HTTP request, kept unless switched off (VOLTER_TWIN_REQUEST_JOURNAL=0).
|
|
29
|
-
// at ~1 MB the file
|
|
30
|
-
//
|
|
28
|
+
// line per served HTTP request, kept unless switched off (VOLTER_TWIN_REQUEST_JOURNAL=0). It is an
|
|
29
|
+
// audit trail, so nothing in it is ever deleted: at ~1 MB the live file closes into the next numbered
|
|
30
|
+
// segment (`requests.jsonl.1`, `.2`, … oldest first) and a fresh one starts. An append that fails is
|
|
31
|
+
// said on stderr and counted per journal (`twinRequestJournalFailures`), never silent. `volter-world tail
|
|
32
|
+
// --requests` merges the live lines.
|
|
31
33
|
//
|
|
32
34
|
// WHAT A LINE CARRIES. `{at, method, path, status}` — the shape — plus, when the request presented
|
|
33
35
|
// one, the CREDENTIAL SHAPE: which header/query-param NAMES carried a credential-looking value,
|
|
@@ -82,21 +84,54 @@ export type TwinRequestJournalEntry = {
|
|
|
82
84
|
ms?: number;
|
|
83
85
|
/** credential-looking headers/query params that arrived, named + fingerprinted, never quoted. */
|
|
84
86
|
credentials?: TwinCredentialShape[];
|
|
87
|
+
/** `request`: written BEFORE the request is served (an audit device's request entry, so a request whose
|
|
88
|
+
* audit cannot be written is refused); its outcome is the entry without a phase written after. */
|
|
89
|
+
phase?: 'request';
|
|
85
90
|
};
|
|
86
91
|
|
|
87
|
-
/** The journal
|
|
88
|
-
|
|
92
|
+
/** The journal's closed segments, oldest first: `requests.jsonl.<n>` beside the live file. */
|
|
93
|
+
function journalSegments(path: string): string[] {
|
|
94
|
+
const base = path.slice(path.lastIndexOf('/') + 1);
|
|
95
|
+
return getActiveWorldStore().list(dirname(path))
|
|
96
|
+
.map((name) => (name.startsWith(`${base}.`) ? Number(name.slice(base.length + 1)) : NaN))
|
|
97
|
+
.filter((n) => Number.isInteger(n) && n > 0)
|
|
98
|
+
.sort((a, b) => a - b)
|
|
99
|
+
.map((n) => `${path}.${n}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The journal read back: every entry kept for a service, oldest first (an empty list when the journal is switched off or empty).
|
|
103
|
+
* With `since` (epoch ms), segments that ended before it are not read. */
|
|
104
|
+
export function readTwinRequestJournal(service: string, root?: string, options: { since?: number } = {}): TwinRequestJournalEntry[] {
|
|
89
105
|
const path = twinRequestJournalPath(service, root);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
for (const line of text.split('\n')) { if (!line) continue; try {
|
|
106
|
+
const parse = (file: string): TwinRequestJournalEntry[] => {
|
|
107
|
+
const text = getActiveWorldStore().read(file); if (!text) return [];
|
|
108
|
+
const rows: TwinRequestJournalEntry[] = [];
|
|
109
|
+
for (const line of text.split('\n')) { if (!line) continue; try { rows.push(JSON.parse(line) as TwinRequestJournalEntry); } catch { /* a torn line */ } }
|
|
110
|
+
return rows;
|
|
111
|
+
};
|
|
112
|
+
const kept: TwinRequestJournalEntry[][] = [parse(path)];
|
|
113
|
+
// newest first, stopping at the first segment that ended before `since`
|
|
114
|
+
for (const file of journalSegments(path).reverse()) {
|
|
115
|
+
const rows = parse(file);
|
|
116
|
+
const last = rows[rows.length - 1]?.at;
|
|
117
|
+
if (options.since !== undefined && last && Date.parse(last) < options.since) break;
|
|
118
|
+
kept.unshift(rows);
|
|
94
119
|
}
|
|
95
|
-
return
|
|
120
|
+
return kept.flat();
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
123
|
+
/** Lines not kept, per journal: keyed by the journal's own path, so a process serving many Worlds
|
|
124
|
+
* (a host; an isolate holding several) answers for each World alone. */
|
|
125
|
+
const journalFailures = new Map<string, number>();
|
|
126
|
+
/** Journal lines this process failed to keep, since it started, for one service's journal under `root`
|
|
127
|
+
* (a World's request report sums its own twins and links). */
|
|
128
|
+
export function twinRequestJournalFailures(service: string, root?: string): number {
|
|
129
|
+
try { return journalFailures.get(twinRequestJournalPath(service, root)) ?? 0; } catch { return 0; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/** ~1 MB per segment: tens of thousands of entries (a shape line is ~80 bytes), and a report over a
|
|
134
|
+
* recent period reads only the segments that reach into it. */
|
|
100
135
|
const REQUEST_JOURNAL_MAX_BYTES = 1_000_000;
|
|
101
136
|
|
|
102
137
|
/**
|
|
@@ -168,32 +203,40 @@ export function twinRequestJournalPath(service: string, root?: string): string {
|
|
|
168
203
|
}
|
|
169
204
|
|
|
170
205
|
/**
|
|
171
|
-
* Append one
|
|
172
|
-
*
|
|
206
|
+
* Append one request to the journal — a no-op only when VOLTER_TWIN_REQUEST_JOURNAL=0. Never throws;
|
|
207
|
+
* answers whether the line was kept (true when the journal is switched off: no audit was chosen), so a
|
|
208
|
+
* caller that must not act unaudited (a link) can refuse.
|
|
173
209
|
*/
|
|
174
|
-
export function journalTwinRequest(service: string, entry: TwinRequestJournalEntry, root?: string):
|
|
175
|
-
if (!twinRequestJournalEnabled()) return;
|
|
210
|
+
export function journalTwinRequest(service: string, entry: TwinRequestJournalEntry, root?: string): boolean {
|
|
211
|
+
if (!twinRequestJournalEnabled()) return true;
|
|
212
|
+
const line = JSON.stringify({
|
|
213
|
+
at: entry.at ?? new Date().toISOString(),
|
|
214
|
+
...(entry.phase ? { phase: entry.phase } : {}),
|
|
215
|
+
method: entry.method.toUpperCase(),
|
|
216
|
+
path: entry.path.split('?')[0], // the query STRING never lands; its credential params do, named + fingerprinted
|
|
217
|
+
status: entry.status,
|
|
218
|
+
...(typeof entry.ms === 'number' ? { ms: entry.ms } : {}),
|
|
219
|
+
...(entry.credentials?.length ? { credentials: entry.credentials } : {}),
|
|
220
|
+
});
|
|
221
|
+
let path: string | undefined;
|
|
176
222
|
try {
|
|
177
223
|
const store = getActiveWorldStore();
|
|
178
|
-
|
|
224
|
+
path = twinRequestJournalPath(service, root);
|
|
179
225
|
store.mkdir(dirname(path));
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
226
|
+
if ((store.stat(path)?.size ?? 0) >= REQUEST_JOURNAL_MAX_BYTES) {
|
|
227
|
+
// the live file closes into the next segment; no segment is ever overwritten or removed (a crash between
|
|
228
|
+
// the two writes leaves the live lines in both: repeated in the next segment, never lost)
|
|
229
|
+
const next = Number(journalSegments(path).pop()?.slice(path.length + 1) ?? 0) + 1;
|
|
230
|
+
store.writeAtomic(`${path}.${next}`, store.read(path) ?? '');
|
|
184
231
|
store.write(path, '');
|
|
185
232
|
}
|
|
186
|
-
const line = JSON.stringify({
|
|
187
|
-
at: entry.at ?? new Date().toISOString(),
|
|
188
|
-
method: entry.method.toUpperCase(),
|
|
189
|
-
path: entry.path.split('?')[0], // the query STRING never lands; its credential params do, named + fingerprinted
|
|
190
|
-
status: entry.status,
|
|
191
|
-
...(typeof entry.ms === 'number' ? { ms: entry.ms } : {}),
|
|
192
|
-
...(entry.credentials?.length ? { credentials: entry.credentials } : {}),
|
|
193
|
-
});
|
|
194
233
|
store.append(path, `${line}\n`);
|
|
195
|
-
|
|
196
|
-
|
|
234
|
+
return true;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const key = path ?? `${root ?? ''}/${service}`;
|
|
237
|
+
journalFailures.set(key, (journalFailures.get(key) ?? 0) + 1);
|
|
238
|
+
console.error(`[request-journal] ${service}: a line was not kept: ${(error as Error).message}`);
|
|
239
|
+
return false;
|
|
197
240
|
}
|
|
198
241
|
}
|
|
199
242
|
|