@mason-data/cli 0.8.0 → 0.9.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 +13 -4
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +505 -99
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +169 -11
- package/dist/src/cli/commands.js +207 -51
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +116 -33
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +97 -6
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +47 -11
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +1 -1
- package/dist/src/cli/main.js +8 -2
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +2 -2
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +99 -0
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +12 -4
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +154 -7
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +63 -7
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +3 -3
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +30 -20
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +115 -11
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +126 -74
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +9 -7
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +5 -0
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +24 -9
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- package/package.json +8 -7
package/dist/src/cli/commands.js
CHANGED
|
@@ -19,16 +19,17 @@ import { err as styleErr, out as styleOut, stepRenderer, wrap } from "./style.js
|
|
|
19
19
|
import { alive, endRun, END_GRACE_MS, notAnswering, runState, theRun } from "./runs.js";
|
|
20
20
|
import { version } from "./version.js";
|
|
21
21
|
import { restartsFact, signalSupervisor, stopSupervisor, supervisorGone } from "./supervise.js";
|
|
22
|
-
import { DEMO_DIR, DUCK_FILE, STATE_DIR, databaseMoved, isProject, projectDatabase, projectSource, readProject, theProject, writeConfig } from "./project.js";
|
|
22
|
+
import { DEMO_DIR, DUCK_FILE, STATE_DIR, databaseMoved, isProject, projectDatabase, projectSource, projectWriteRefusal, readProject, theProject, writeConfig } from "./project.js";
|
|
23
23
|
import { callRun, RunUnreachable } from "./control.js";
|
|
24
24
|
import { withPlaceholders, postgresFacts, postgresLink } from "./pg-url.js";
|
|
25
25
|
import { listenerText } from "./pg-tls.js";
|
|
26
|
-
import {
|
|
26
|
+
import { entriesOfLines, logLineText } from "../lib/log-buffer.js";
|
|
27
|
+
import { LogReader, logPath } from "../lib/log-read.js";
|
|
27
28
|
import { catalogTable, entryName, looksSecret, tableNameProblem } from "./sources.js";
|
|
28
29
|
import { addEntry, Refusal, removeEntries, sourceToEdit } from "./source-edit.js";
|
|
29
30
|
import { connectSource, maskedUrl, parseSource, readSourceFacts } from "./preflight.js";
|
|
30
31
|
import { reach } from "./state.js";
|
|
31
|
-
import { copyingText, copyProgress, duration, msText, readiness, readinessWord } from "./readiness.js";
|
|
32
|
+
import { copyingText, copyProgress, duration, generatedReadiness, msText, readiness, readinessWord } from "./readiness.js";
|
|
32
33
|
import { pathText } from "../duck/kept-route.js";
|
|
33
34
|
import { DEFAULT_REQUEST_LIMIT, eventLimit, MAX_SEARCH, SLOW_MS, requestFilter, requestFilterParams } from "./request-filter.js";
|
|
34
35
|
import { countsText } from "../duck/stream.js";
|
|
@@ -104,7 +105,8 @@ export { copyingText, copyProgress, duration, msText, readiness, readinessWord }
|
|
|
104
105
|
/** A table line's bar in its state's colour (on a terminal): green copied, red failed, yellow served but not kept current, cyan at work
|
|
105
106
|
* (a table whose sync is off is copied as it asked: green). */
|
|
106
107
|
export function paintBar(line, s) {
|
|
107
|
-
const colour = / its first sync failed:
|
|
108
|
+
const colour = / its first sync failed: | NOT kept current, its sync failed /.test(line) ? s.red : / NOT kept current | (—|·) behind by /.test(line) ? s.yellow
|
|
109
|
+
: / 100% [\d,]+ rows · synced( · |$)/.test(line) ? s.green : s.cyan;
|
|
108
110
|
return line.replace(/(█+)(░*)|(░+)/, (_m, full, empty, none) => full !== undefined ? `${colour(full)}${s.dim(empty ?? "")}` : s.dim(none ?? ""));
|
|
109
111
|
}
|
|
110
112
|
/** The width of a table's progress bar, in characters. */
|
|
@@ -127,12 +129,12 @@ export function tableLines(s, now = Date.now()) {
|
|
|
127
129
|
let fraction, what;
|
|
128
130
|
if (r === 0) {
|
|
129
131
|
fraction = 1;
|
|
130
|
-
const age =
|
|
132
|
+
const age = keptWords(t, now);
|
|
131
133
|
what = `${n(t.copy?.rowsCopied ?? 0)} rows · synced${t.copy?.retired ? ` · ${t.copy.retired}` : ""}${age ? ` · ${age}` : ""}`;
|
|
132
134
|
}
|
|
133
135
|
else if (r === 1) {
|
|
134
136
|
fraction = 1;
|
|
135
|
-
what = `its first sync failed: ${firstLine(t.error)}`;
|
|
137
|
+
what = t.copy?.phase === "done" ? copiedFailedText(t, now) : `its first sync failed: ${firstLine(t.error)}`;
|
|
136
138
|
}
|
|
137
139
|
else if (r === 2) {
|
|
138
140
|
const c = copyingText(t.copy.rowsCopied, t.rows, t.copy.progress);
|
|
@@ -163,7 +165,8 @@ export function keptAge(t, now = Date.now()) {
|
|
|
163
165
|
if (k.state !== "streamed")
|
|
164
166
|
return `NOT kept current (${k.syncedAt === null ? "never synced" : `last synced ${agoText(k.syncedAt, now)}`}): ${k.why ?? "it is not streamed yet"}`;
|
|
165
167
|
const at = `replicated, at ${k.position}, ${k.syncedAt === null ? "not synced yet" : agoText(k.syncedAt, now)}`;
|
|
166
|
-
|
|
168
|
+
// behind: `keptWords` says it, with the stream not running as its why
|
|
169
|
+
return k.notRunning === null || t.sync?.state === "behind" ? at : `${at} — NOT kept current now, the stream is not running: ${k.notRunning}`;
|
|
167
170
|
}
|
|
168
171
|
if (k?.by !== "poll")
|
|
169
172
|
return null;
|
|
@@ -175,6 +178,33 @@ export function keptAge(t, now = Date.now()) {
|
|
|
175
178
|
return `sync: off, ${synced} — mason sync ${tableArg(t.table)} syncs it now; sync: poll 5m on its entry polls it`;
|
|
176
179
|
return `polled every ${k.sync.every}, ${k.syncedAt === null ? "not polled yet" : `last ${agoText(k.syncedAt, now)}`}`;
|
|
177
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* WHAT A SERVED TABLE'S LINE SAYS AFTER `synced`: how it is kept and how old it is (`keptAge`, by the kind of its
|
|
183
|
+
* keeping), then — for EVERY kind, from the one word (`tables[].sync.state`, src/cli/readiness.ts behindOf), never a
|
|
184
|
+
* second reading of `kept` — behind by how much and why. A kind keptAge has no words for (recopy) still says behind;
|
|
185
|
+
* a why its words already say is not said twice. Null: nothing to say.
|
|
186
|
+
*/
|
|
187
|
+
export function keptWords(t, now = Date.now()) {
|
|
188
|
+
const age = keptAge(t, now), s = t.sync;
|
|
189
|
+
if (s?.state !== "behind" || s.behindBySeconds == null)
|
|
190
|
+
return age;
|
|
191
|
+
const why = s.behindWhy ?? "its next sync is late";
|
|
192
|
+
const by = `behind by ${duration(s.behindBySeconds)}${age !== null && age.includes(why) ? "" : `: ${why}`}`;
|
|
193
|
+
return age === null ? by : `${age} — ${by}`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A SERVED TABLE WHOSE SYNC FAILED (its first sync succeeded; its poll, catch-up, stream or publication check did not:
|
|
197
|
+
* issues 100, 82): served as of its age, NOT kept current, why — how it is kept, when it says it is not kept current
|
|
198
|
+
* now (`keptAge`: a catch-up waiting out its back-off, a table the publication does not carry, with the way out), else
|
|
199
|
+
* its error's first line — never "its first sync failed".
|
|
200
|
+
*/
|
|
201
|
+
function copiedFailedText(t, now) {
|
|
202
|
+
const k = t.kept;
|
|
203
|
+
const reason = k?.by === "replication" ? (k.state !== "streamed" ? k.why : k.notRunning === null ? null : `the stream is not running: ${k.notRunning}`)
|
|
204
|
+
: k?.by === "poll" || k?.by === "recopy" ? k.pending ?? null : null;
|
|
205
|
+
const asOf = t.sync?.asOf ?? (t.kept && t.kept.by !== "generated" && t.kept.by !== "recopy" ? t.kept.syncedAt : null) ?? t.sync?.syncedAt ?? null;
|
|
206
|
+
return `${n(t.copy?.rowsCopied ?? 0)} rows · synced · NOT kept current, its sync failed (${asOf === null ? "never synced" : `last synced ${agoText(asOf, now)}`}): ${reason ?? firstLine(t.error)}`;
|
|
207
|
+
}
|
|
178
208
|
/** A span of seconds as the stream's lines say it: `0.3 s` under ten seconds, then as a wait reads (`duration`). */
|
|
179
209
|
export const spanText = (seconds) => seconds < 10 ? `${Math.max(0, seconds).toFixed(1)} s` : duration(seconds);
|
|
180
210
|
/**
|
|
@@ -250,18 +280,22 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
250
280
|
<user>:<password> in the credentials' place), psql (its psql line), listener:
|
|
251
281
|
{ host (what it is bound to), tls (certificate, self-signed or null), fingerprint } }, source: { name (the
|
|
252
282
|
source's: its sources file's), file (the sources file), shownFile (it from the project's root), url, database, remote, version, standby, changes,
|
|
253
|
-
replication, demo (the demo database this run hosts), sync: { default (every table's sync unless its entry
|
|
283
|
+
replication, demo (the demo database this run hosts), textOrder (what reading the database's text order left to say), sync: { default (every table's sync unless its entry
|
|
254
284
|
says its own: off, poll 30s), written (the file's top sync: says it; false: the default) } }, warnings, folder, project, tables: [{ table, rows,
|
|
255
|
-
bytes, key, added, addedColumns, copy, error
|
|
285
|
+
bytes, key, added, addedColumns, copy, error (why its sync failed: its first sync, or the poll, catch-up, stream or
|
|
286
|
+
publication check keeping a synced table current; null otherwise), kept, sync: { state (synced, syncing,
|
|
287
|
+
behind: served and kept but older than its sync: promises, failed, waiting), syncedAt (when
|
|
256
288
|
it was last brought current), syncSeconds (how long that took), syncedBy (copy, poll, catch-up, stream),
|
|
257
|
-
ageSeconds (how long ago that was)
|
|
289
|
+
ageSeconds (how long ago that was), asOf (the time the synced data holds the database as of: kept's syncedAt),
|
|
290
|
+
promisedSeconds (the age its sync: promises not to pass; null: sync: off promises nothing), behindBySeconds
|
|
291
|
+
(how far past it, 0 when not; null when nothing is promised), behindWhy (why, while behind; else null) }, … }], totals: { added, copied, failed, rows (the synced tables' rows),
|
|
258
292
|
syncedAt (the latest sync of any) }, engine: { file, bytes, keptDays (MASON_HISTORY_DAYS), version (the run's), startedAt,
|
|
259
293
|
uptimeSeconds, restarts (the times its supervisor started the run again: { count, last: { at, why, upSeconds }
|
|
260
294
|
or null }, or { error }) }, disk: { path (where the synced data's file is), freeBytes, copyBytes (the file),
|
|
261
295
|
projectedBytes (what the first syncs running are projected to add; null: none, or too early to say), error },
|
|
262
296
|
sessions: { open (the Postgres API's sessions logged in), byClient: [{ user ('' a token's), client (its
|
|
263
297
|
application_name), open, running }] }, share: (null, or what mason share has open) { port, fingerprint, sessions,
|
|
264
|
-
refusedLogins, droppedConnections, shares: [{ user, role, reads, openedAt, via, host, port, endsAt, state }] }, queries: { keptDays, day, week } (the last day's and the last 7
|
|
298
|
+
refusedLogins, droppedConnections, shares: [{ user, from (command: mason share; page: the web page), role, reads, openedAt, via, host, port, endsAt, state }] }, shareEnded: (null, or the last share that ended) { user, from, why, at }, queries: { keptDays, day, week } (the last day's and the last 7
|
|
265
299
|
days' statements, each { days, covered (the days of it the request log keeps), statements, completed, refused,
|
|
266
300
|
failed, cancelled, rows, bytesSent, durationMs: { avg, p50, p95 } (the completed ones') or null }; { error }
|
|
267
301
|
when it could not be read), slowQueries, refusedQueries, refusedTotal (how many in all), unsupported: [{ gap
|
|
@@ -272,15 +306,21 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
272
306
|
newest last: mason logs --events), error (why they could not be read, or null), queries: [{ requestId,
|
|
273
307
|
startedAt, outcome, sqlstate, message, query, … }] (the latest statements refused, failed or cancelled, newest
|
|
274
308
|
last, as mason logs --requests --json shows each: mason logs --requests --failed), queriesError (or null) },
|
|
275
|
-
readiness (what mason wait waits on), ui (whether this run serves the web page: mason ui),
|
|
309
|
+
readiness (what mason wait waits on), ui (whether this run serves the web page: mason ui), served (this is
|
|
310
|
+
mason serve's run: mason ui --ssh opens its page), sourcesReadOnly (why the page offers no Add or Remove — the
|
|
311
|
+
project's files deployed read-only, MASON_PROJECT_READONLY, which mason add and mason remove refuse too, or a
|
|
312
|
+
server's sources file it cannot write — or null), next }.
|
|
276
313
|
Each table's kept is how it is kept current now (absent for a table the sync does not hold):
|
|
277
314
|
{ by: poll, sync: { mode: off } or { mode: poll, every (30s), seconds } (its entry's sync:, else the file's,
|
|
278
315
|
else off), syncedAt (its last whole read or poll), nextAt (when its next poll is due; null: off, or not polled
|
|
279
316
|
now), pending (only when it applies: why it is not kept current now, its phase included: it waits for
|
|
280
317
|
its first sync, it is being read whole, its first sync failed, it is read again whole, caught up first) }
|
|
281
|
-
or { by: recopy, copiedAt, pending (only when it applies, as the poll's) }
|
|
318
|
+
or { by: recopy, copiedAt, syncedAt, pending (only when it applies, as the poll's) }
|
|
282
319
|
or { by: replication, slot, state (streamed, catching-up, waiting, left-out), why, position, lastCommit,
|
|
283
|
-
notRunning (why the stream is not running, null when it is) }; with
|
|
320
|
+
notRunning (why the stream is not running, null when it is) }; each with promisedSeconds (the age of
|
|
321
|
+
its synced data past which it is behind: poll, its interval plus the larger of 60 s and twice its last poll;
|
|
322
|
+
replication, 120 s; off, null) and lateWhy (what would make it late now: the pass's or its own poll's
|
|
323
|
+
time, which Mason waits four times, the stream not running); with Mason stopped: { running: false, project, source: { name,
|
|
284
324
|
file, url, error (the sources file's problem, or null) }, engine, tables: [{ table }], lastRun, lastStop:
|
|
285
325
|
{ at, how } or null, next }; hung: the same with hung: { pid, problem, why },
|
|
286
326
|
exit 1. In replication mode stream: { slot, slotWal: { walHeldBytes (the WAL the slot holds on the
|
|
@@ -294,7 +334,11 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
294
334
|
fill: { rows, bytesPerRow, seconds, basis (measured: from the last fill of its column; disk or text: modelled from
|
|
295
335
|
the column's bytes a row on disk, or its text's before it is on disk), rowsPerSecond } (the fill's estimate it
|
|
296
336
|
was decided on), filled: { rows, seconds } (what its own fill measured), queued (a backfill waiting for the fill
|
|
297
|
-
under way), keptAt, readyAt, why }].
|
|
337
|
+
under way), keptAt, readyAt, why }]. generatedTables: [{ table, file, kind, method (whole: its SQL run again),
|
|
338
|
+
state (refused, waiting, building, failed, current, behind), targetLag, behindSeconds (across its chain: the
|
|
339
|
+
oldest base change below it not in it), behindSince (when that change was committed; null: none), missingTarget, serving,
|
|
340
|
+
lastRun: { at, seconds, rows }, error (the last run's, no value of a row in it), retryAt,
|
|
341
|
+
refusals: [{ key, message, why, options, text }], waitingFor, warnings }]. roles (a run with a roles file, mason/roles.yml: what a user added
|
|
298
342
|
with --role may read): { file, roles: [name], problem (what is wrong with the file now, every statement of a
|
|
299
343
|
user with a role refused while there is; null), default (the role of a user added without --role; null: the
|
|
300
344
|
file names none, and such a user reads nothing), all (the roles of tables: all) }
|
|
@@ -318,9 +362,9 @@ export const STATUS = {
|
|
|
318
362
|
summary: "what Mason is doing: the database, each synced table, the Postgres API, its statements and errors",
|
|
319
363
|
usage: STATUS_USAGE,
|
|
320
364
|
options: { json: { type: "boolean" } },
|
|
321
|
-
json: ["running", "pid", "postgres", "source", "warnings", "folder", "project", "tables", "totals", "engine", "disk", "sessions", "stream", "jsonPaths", "queries", "slowQueries", "refusedQueries",
|
|
322
|
-
"refusedTotal", "unsupported", "recentErrors", "readiness", "ui",
|
|
323
|
-
"roles", "share", "lastRun", "lastStop", "hung", "next"],
|
|
365
|
+
json: ["running", "pid", "postgres", "source", "warnings", "folder", "project", "tables", "totals", "engine", "disk", "sessions", "stream", "jsonPaths", "generatedTables", "queries", "slowQueries", "refusedQueries",
|
|
366
|
+
"refusedTotal", "unsupported", "recentErrors", "readiness", "ui", "served", "sourcesReadOnly",
|
|
367
|
+
"roles", "share", "shareEnded", "lastRun", "lastStop", "hung", "next"],
|
|
324
368
|
};
|
|
325
369
|
export async function status(argv) {
|
|
326
370
|
const f = parseCommand(STATUS, argv);
|
|
@@ -396,7 +440,7 @@ export async function status(argv) {
|
|
|
396
440
|
["Listener", listenerText(run.postgresListener, postgresLink(run.postgresUrl).port)],
|
|
397
441
|
...shareRows(s.share),
|
|
398
442
|
// the web page (mason ui), without its one-time token: mason ui prints the URL that carries it, and opens it
|
|
399
|
-
...(s.ui === true ? [["Web UI", `${run.control.url}/ (mason ui opens it)`]] : []),
|
|
443
|
+
...(s.ui === true ? [["Web UI", s.served === true ? "from your machine: mason ui --ssh <user@server>" : `${run.control.url}/ (mason ui opens it)`]] : []),
|
|
400
444
|
// the roles file as the listener reads it now (mason user add --role): its roles, or what is wrong with it
|
|
401
445
|
...(s.roles && s.roles.file !== null
|
|
402
446
|
? [["Roles", s.roles.problem !== null ? st.red(`${s.roles.problem} (every statement of a user with a role is refused until it is fixed)`)
|
|
@@ -424,6 +468,8 @@ export async function status(argv) {
|
|
|
424
468
|
out(l);
|
|
425
469
|
for (const l of jsonPathLines(s.jsonPaths ?? [], st))
|
|
426
470
|
out(l);
|
|
471
|
+
for (const l of generatedLines(s.generatedTables ?? [], st))
|
|
472
|
+
out(l);
|
|
427
473
|
for (const l of queryLines(queries, st))
|
|
428
474
|
out(l);
|
|
429
475
|
for (const l of slowLines(slowReply.status === 200 ? slow : { error: String(slowReply.body.error ?? slowReply.status) }, st))
|
|
@@ -459,6 +505,7 @@ export function sourceRows(s) {
|
|
|
459
505
|
`${s.source.replication.owned ? "Mason's own, carrying them alone" : "named: read as it is, never altered"}); the others by a SELECT of their changed rows`
|
|
460
506
|
: s.source.demo ? "the demo database is checked this way only"
|
|
461
507
|
: "sync: replication on an entry streams that table as it happens instead")],
|
|
508
|
+
...(s.source.textOrder ? [["Text order", s.source.textOrder]] : []),
|
|
462
509
|
];
|
|
463
510
|
}
|
|
464
511
|
/** An SQL statement on one line, cut at `max` characters (the whole of it is mason logs --requests --json's). */
|
|
@@ -487,6 +534,38 @@ export function jsonPathLines(paths, st) {
|
|
|
487
534
|
lines.push(` ${st.dim(`${dropped} dropped (mason status --json says why)`)}`);
|
|
488
535
|
return lines;
|
|
489
536
|
}
|
|
537
|
+
/** a generated table's state in the words mason status prints: how it is kept, how far behind, its last run */
|
|
538
|
+
function generatedWords(g) {
|
|
539
|
+
const how = `${g.kind ?? "?"} ${g.method}, target ${g.targetLag ?? "?"}`;
|
|
540
|
+
const last = g.lastRun === null ? "" : `; last re-run ${duration(g.lastRun.seconds)}, ${n(g.lastRun.rows)} row${g.lastRun.rows === 1 ? "" : "s"}`;
|
|
541
|
+
if (g.state === "waiting")
|
|
542
|
+
return `${how}, waiting for ${g.waitingFor ?? "a table"} to sync`;
|
|
543
|
+
if (g.state === "building")
|
|
544
|
+
return `${how}, being built`;
|
|
545
|
+
if (g.state === "refused")
|
|
546
|
+
return g.serving ? `${how}, its file is refused; it keeps serving what it was built from${last}` : "refused";
|
|
547
|
+
const lag = g.behindSeconds === 0 ? "nothing behind" : `${duration(g.behindSeconds)} behind`;
|
|
548
|
+
const missed = g.missingTarget ? ", past its target" : "";
|
|
549
|
+
const failed = g.error === null ? "" : `; its last run failed, tried again at ${g.retryAt ?? "?"}: ${g.error}`;
|
|
550
|
+
return `${how}, ${lag}${missed}${last}${failed}`;
|
|
551
|
+
}
|
|
552
|
+
/** mason status's generated tables: a line each, and a refused one's reasons and ways out under it, whole */
|
|
553
|
+
export function generatedLines(tables, st) {
|
|
554
|
+
if (tables.length === 0)
|
|
555
|
+
return [];
|
|
556
|
+
const width = Math.max(...tables.map((g) => entryName(g.table).length));
|
|
557
|
+
const lines = [`\n${st.bold("Generated tables")} ${st.dim("(tables declared in SQL; mason help generated)")}:`];
|
|
558
|
+
for (const g of tables) {
|
|
559
|
+
const words = generatedWords(g);
|
|
560
|
+
const paint = g.state === "refused" || g.state === "failed" || g.missingTarget ? st.red : g.state === "current" ? st.green : g.state === "behind" ? (x) => x : st.yellow;
|
|
561
|
+
lines.push(` ${entryName(g.table).padEnd(width)} ${paint(words)}`);
|
|
562
|
+
for (const r of g.refusals)
|
|
563
|
+
lines.push(...r.text.split("\n").map((l) => ` ${l}`));
|
|
564
|
+
for (const w of g.warnings)
|
|
565
|
+
lines.push(` ${st.yellow(w)}`);
|
|
566
|
+
}
|
|
567
|
+
return lines;
|
|
568
|
+
}
|
|
490
569
|
/** The copies in all, as mason status's `Copies` row says them: `3 of 3 added synced, 1,234 rows, the last synced 5 s ago`. */
|
|
491
570
|
export function totalsText(t, now = Date.now()) {
|
|
492
571
|
const last = t.syncedAt === null ? "" : `, the last synced ${duration((now - Date.parse(t.syncedAt)) / 1000)} ago`;
|
|
@@ -815,9 +894,9 @@ async function readCatalog(p) {
|
|
|
815
894
|
return src;
|
|
816
895
|
const demo = p.config.demo === true;
|
|
817
896
|
const fix = `change the URL where it is (${src.from}) when the database's password or host changed`;
|
|
818
|
-
const parsed = parseSource(src.resolved);
|
|
897
|
+
const parsed = parseSource(src.resolved, src.from);
|
|
819
898
|
if ("problem" in parsed)
|
|
820
|
-
return { message:
|
|
899
|
+
return { message: parsed.problem, code: "bad_source", fix };
|
|
821
900
|
const connected = await connectSource(parsed.source, { demo });
|
|
822
901
|
if ("failure" in connected) {
|
|
823
902
|
// the demo database's way out is its run (src/cli/demo.ts): its hint is the fix, mason start the next command
|
|
@@ -925,7 +1004,8 @@ const ADD_USAGE = `usage: mason add <table>… [--columns <column>,…] [--inclu
|
|
|
925
1004
|
With Mason running the first sync starts at once; mason wait returns when it is done and the table is served. Before
|
|
926
1005
|
mason start (mason init && mason add orders && mason start) the entry is written, checked against the database as
|
|
927
1006
|
the run checks it, and the first sync begins at mason start. Adding a table again with other columns changes its entry,
|
|
928
|
-
and the table is synced again whole with them.
|
|
1007
|
+
and the table is synced again whole with them. A project deployed as committed (MASON_PROJECT_READONLY=1 where Mason
|
|
1008
|
+
runs) is refused, its files left as they are: add the table where the project is committed, then deploy.
|
|
929
1009
|
|
|
930
1010
|
--columns <a,b,…> only these columns (one table at a time: the list is that table's)
|
|
931
1011
|
--include <a,b,…> these columns as well as every other one — a column that looks like a secret, named on purpose
|
|
@@ -1003,6 +1083,11 @@ export async function add(argv) {
|
|
|
1003
1083
|
// (a state with no project never meets the need: reach said it)
|
|
1004
1084
|
if (where === null || where.project === null)
|
|
1005
1085
|
return 1;
|
|
1086
|
+
// a project whose files are deployed read-only (src/cli/project.ts projectWriteRefusal): refused before the run or the
|
|
1087
|
+
// database is asked, whether Mason is up or down — the run's own record counts, whatever this shell's environment says
|
|
1088
|
+
const readOnly = projectWriteRefusal(where.project.root, process.env);
|
|
1089
|
+
if (readOnly !== null)
|
|
1090
|
+
return failed("add", readOnly, { json });
|
|
1006
1091
|
if (where.state === "up") {
|
|
1007
1092
|
// Mason up: the run adds it (the copy starts now, and the run's own re-read never races the file's write)
|
|
1008
1093
|
const run = where.run;
|
|
@@ -1132,7 +1217,9 @@ const REMOVE_USAGE = `usage: mason remove <table>… [--json]
|
|
|
1132
1217
|
|
|
1133
1218
|
Takes tables out of the project: each one's entry in the sources file (mason/sources/<name>.yml) is removed, the
|
|
1134
1219
|
rest of the file as it was. With Mason running it stops syncing the table and drops its synced data at once; with Mason
|
|
1135
|
-
stopped, what an earlier run synced is dropped at the next mason start.
|
|
1220
|
+
stopped, what an earlier run synced is dropped at the next mason start. A project deployed as committed
|
|
1221
|
+
(MASON_PROJECT_READONLY=1 where Mason runs) is refused, its files left as they are: remove it where the project is
|
|
1222
|
+
committed, then deploy.
|
|
1136
1223
|
|
|
1137
1224
|
mason remove customers
|
|
1138
1225
|
mason remove customers billing.invoices several at once
|
|
@@ -1160,6 +1247,10 @@ export async function remove(argv) {
|
|
|
1160
1247
|
// (a state with no project never meets the need: reach said it)
|
|
1161
1248
|
if (where === null || where.project === null)
|
|
1162
1249
|
return 1;
|
|
1250
|
+
// read-only, as mason add (src/cli/project.ts projectWriteRefusal): refused before the run or the database is asked
|
|
1251
|
+
const readOnly = projectWriteRefusal(where.project.root, process.env);
|
|
1252
|
+
if (readOnly !== null)
|
|
1253
|
+
return failed("remove", readOnly, { json });
|
|
1163
1254
|
if (where.state === "up") {
|
|
1164
1255
|
// Mason up: the run removes it and drops the copy at once (src/cli/run-server.ts removeTables)
|
|
1165
1256
|
const run = where.run;
|
|
@@ -1201,15 +1292,18 @@ export function removedReport(removed, file, o = {}) {
|
|
|
1201
1292
|
/** An added table as mason wait --json says it: its copy, its error, and how it is kept current, as the run's state has
|
|
1202
1293
|
* them (mason status --json's own fields, never derived again here) */
|
|
1203
1294
|
export function waitTable(t) {
|
|
1204
|
-
return { table: t.table, copy: t.copy, error: t.error, ...(t.kept ? { kept: t.kept } : {}) };
|
|
1295
|
+
return { table: t.table, copy: t.copy, error: t.error, ...(t.kept ? { kept: t.kept } : {}), ...(t.sync ? { sync: t.sync } : {}) };
|
|
1205
1296
|
}
|
|
1206
|
-
const WAIT_USAGE = `usage: mason wait [--timeout <seconds>] [--json]
|
|
1297
|
+
const WAIT_USAGE = `usage: mason wait [<generated table>] [--timeout <seconds>] [--json]
|
|
1207
1298
|
|
|
1208
1299
|
Waits until every added table is synced and served, showing every added table on a line of its own: 100% for the
|
|
1209
1300
|
finished (in the order they finished), a bar for the ones still syncing, at the bottom. Run it after mason add.
|
|
1301
|
+
Named, a generated table instead: waits until it holds every change Mason had synced when the wait began, across its
|
|
1302
|
+
chain (the tables it is generated from, and theirs), so what is read from it next includes them.
|
|
1210
1303
|
|
|
1211
1304
|
mason add orders && mason wait
|
|
1212
1305
|
mason wait --timeout 3600 a large table: wait up to an hour
|
|
1306
|
+
mason wait daily_requests a generated table: until it holds what is synced now
|
|
1213
1307
|
|
|
1214
1308
|
--timeout <seconds> how long to wait (default 600); the sync goes on after, and mason wait again picks it up
|
|
1215
1309
|
--json as data, once it ends: { ready, failed, waiting, timedOut, tables: [{ table, copy, error,
|
|
@@ -1217,19 +1311,36 @@ const WAIT_USAGE = `usage: mason wait [--timeout <seconds>] [--json]
|
|
|
1217
1311
|
rate, the seconds left, while its chunks are read }; kept as mason status --json says it —
|
|
1218
1312
|
{ by: poll, sync: { mode: off } or { mode: poll, every, seconds }, syncedAt (its last whole read
|
|
1219
1313
|
or poll), nextAt (its next poll; null: off) }; kept's pending, when it applies, says why
|
|
1220
|
-
it is not kept current now, its phase included
|
|
1314
|
+
it is not kept current now, its phase included; sync as mason status --json says it: its state —
|
|
1315
|
+
synced, syncing, behind, failed, waiting — asOf, promisedSeconds, behindBySeconds, behindWhy),
|
|
1221
1316
|
stream (replication mode: where the stream is, as mason status --json says it), error (on a
|
|
1222
1317
|
failure or a timeout: { code, message, next }), next }
|
|
1318
|
+
Named, a generated table: the same ready, failed, waiting, timedOut, error and next, tables
|
|
1319
|
+
empty, and generatedTable (as mason status --json's generatedTables says it: its state,
|
|
1320
|
+
behindSeconds, behindSince — when the oldest change not in it was committed, null: none)
|
|
1223
1321
|
|
|
1224
|
-
Exit status: 0 ready
|
|
1322
|
+
Exit status: 0 ready (a table behind what its sync: promises is said on its line, and ready); 1 a first sync failed,
|
|
1323
|
+
or the sync keeping a synced table current failed (its poll, its catch-up, the stream, or a named publication that
|
|
1324
|
+
does not carry it), or the generated table's run failed, or it or a parent was refused
|
|
1325
|
+
before it was built (printed, with what to do; Mason tries a failed one again by itself); 124 timed out.
|
|
1326
|
+
A generated table that serves and holds everything synced is ready (0) though its last run failed: after editing its
|
|
1327
|
+
file, read mason status for whether the new definition was refused or failed.
|
|
1225
1328
|
`;
|
|
1329
|
+
/** how often a wait not drawn in place says its block again though nothing in it changed, so a long one never goes
|
|
1330
|
+
* quiet: the copies' wait and a generated table's */
|
|
1331
|
+
export const SAY_AGAIN_MS = 30_000;
|
|
1332
|
+
/** whether a wait says its block now: what it is changed (`what`, its numbers taken out), or `every` ms went by since it
|
|
1333
|
+
* last said it */
|
|
1334
|
+
export function sayAgain(what, said, saidAt, now, every = SAY_AGAIN_MS) {
|
|
1335
|
+
return what !== said || now - saidAt >= every;
|
|
1336
|
+
}
|
|
1226
1337
|
export const WAIT = {
|
|
1227
1338
|
needs: "run",
|
|
1228
1339
|
name: "wait",
|
|
1229
|
-
summary: "wait until every added table is synced
|
|
1340
|
+
summary: "wait until every added table is synced, or a generated table holds what is synced now (1: a failure)",
|
|
1230
1341
|
usage: WAIT_USAGE,
|
|
1231
1342
|
options: { json: { type: "boolean" }, timeout: { type: "string" } },
|
|
1232
|
-
json: ["ready", "failed", "waiting", "timedOut", "tables", "stream", "error", "next"],
|
|
1343
|
+
json: ["ready", "failed", "waiting", "timedOut", "tables", "stream", "generatedTable", "error", "next"],
|
|
1233
1344
|
};
|
|
1234
1345
|
export async function wait(argv) {
|
|
1235
1346
|
const f = parseCommand(WAIT, argv);
|
|
@@ -1239,9 +1350,14 @@ export async function wait(argv) {
|
|
|
1239
1350
|
const timeout = f.values.timeout === undefined ? 600 : Number(f.values.timeout);
|
|
1240
1351
|
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
1241
1352
|
return failed("wait", { message: `--timeout is a number of seconds, not ${JSON.stringify(f.values.timeout)}`, code: "usage" }, { json });
|
|
1353
|
+
if (f.positionals.length > 1)
|
|
1354
|
+
return failed("wait", { message: `name one generated table, not ${f.positionals.length}`, code: "usage", usage: WAIT_USAGE }, { json });
|
|
1355
|
+
const named = f.positionals[0];
|
|
1242
1356
|
const run = await runOr(WAIT, json);
|
|
1243
1357
|
if (!run)
|
|
1244
1358
|
return 1;
|
|
1359
|
+
if (named !== undefined)
|
|
1360
|
+
return waitGenerated(run, named, timeout, json);
|
|
1245
1361
|
return guarded("wait", json, async () => {
|
|
1246
1362
|
const until = Date.now() + timeout * 1000;
|
|
1247
1363
|
// a terminal gets the block — every added table on its line (tableLines), then the stream — redrawn in place;
|
|
@@ -1266,7 +1382,7 @@ export async function wait(argv) {
|
|
|
1266
1382
|
}
|
|
1267
1383
|
// the same work, other numbers (a stream's positions included: they move with every change)
|
|
1268
1384
|
const what = lines.join("\n").replace(/[█░]+/g, "").replace(/\b[0-9A-F]+\/[0-9A-F]+\b/g, "#").replace(/[\d,.]+(%| rows\/s| s| min| h)?/g, "#");
|
|
1269
|
-
if (what
|
|
1385
|
+
if (sayAgain(what, said, saidAt, Date.now())) {
|
|
1270
1386
|
err(lines.map((l) => ` ${l}`).join("\n"));
|
|
1271
1387
|
said = what;
|
|
1272
1388
|
saidAt = Date.now();
|
|
@@ -1309,6 +1425,50 @@ export async function wait(argv) {
|
|
|
1309
1425
|
}
|
|
1310
1426
|
});
|
|
1311
1427
|
}
|
|
1428
|
+
/** `mason wait <generated table>`: until it holds every change synced when the wait began (src/cli/readiness.ts
|
|
1429
|
+
* generatedReadiness), a line when what it waits for changes; the same outcome and exit codes as the wait for copies */
|
|
1430
|
+
async function waitGenerated(run, named, timeout, json) {
|
|
1431
|
+
return guarded("wait", json, async () => {
|
|
1432
|
+
const since = Date.now(), until = since + timeout * 1000;
|
|
1433
|
+
let said = "", saidAt = 0;
|
|
1434
|
+
for (;;) {
|
|
1435
|
+
const s = await readState(run);
|
|
1436
|
+
const all = s.generatedTables ?? [];
|
|
1437
|
+
const g = all.find((t) => t.table === named) ?? all.find((t) => t.table === `public.${named}`);
|
|
1438
|
+
if (g === undefined) {
|
|
1439
|
+
const names = all.map((t) => entryName(t.table));
|
|
1440
|
+
return failed("wait", { message: `no generated table ${JSON.stringify(named)}${names.length ? "" : ": the project has none"}`, code: "not_found",
|
|
1441
|
+
...(names.length ? { fix: `name one of: ${names.join(", ")}` } : {}), next: [["mason status", "the generated tables and what each is doing"]] }, { json });
|
|
1442
|
+
}
|
|
1443
|
+
const r = generatedReadiness(g, since);
|
|
1444
|
+
const next = [["mason status", "the generated tables and what each is doing"]];
|
|
1445
|
+
const failure = r.failed.length
|
|
1446
|
+
? { message: r.failed[0], code: "failed", fix: "Mason tries a failed run again by itself; mason logs says why it failed", next: [["mason logs", "the detail of the failure"], ...next] }
|
|
1447
|
+
: !r.ready && Date.now() >= until
|
|
1448
|
+
? { message: `${g.table} does not hold what was synced ${timeout} s ago yet`, code: "timeout", why: r.waiting.join("\n"), fix: "nothing stopped: its runs go on",
|
|
1449
|
+
next: [[`mason wait ${entryName(g.table)}`, "keep waiting (--timeout <seconds> for longer)"], ...next] }
|
|
1450
|
+
: null;
|
|
1451
|
+
if (json && (r.ready || failure)) {
|
|
1452
|
+
out(jsonText(WAIT, { ...r, timedOut: !r.ready && failure?.code === "timeout", tables: [], generatedTable: g, ...(failure ? { error: errorJson(failure) } : {}) }, failure ? failure.next ?? [] : next));
|
|
1453
|
+
}
|
|
1454
|
+
if (r.ready) {
|
|
1455
|
+
if (!json)
|
|
1456
|
+
out(withEnd(styleOut().green(`Ready: ${g.table} holds every change synced when the wait began.`), { next }));
|
|
1457
|
+
return 0;
|
|
1458
|
+
}
|
|
1459
|
+
if (failure)
|
|
1460
|
+
return json ? (failure.code === "timeout" ? 124 : 1) : failed("wait", failure, { json: false });
|
|
1461
|
+
const line = r.waiting.join("\n").replace(/\d+ s behind/, "# s behind");
|
|
1462
|
+
// when what it waits for changes, and every 30 s while it does not, as the copies' wait says its block
|
|
1463
|
+
if (!json && sayAgain(line, said, saidAt, Date.now())) {
|
|
1464
|
+
err(` ${r.waiting.join("\n ")}`);
|
|
1465
|
+
said = line;
|
|
1466
|
+
saidAt = Date.now();
|
|
1467
|
+
}
|
|
1468
|
+
await new Promise((res) => setTimeout(res, 1000));
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1312
1472
|
// ------------------------------------------------------------------ mason logs
|
|
1313
1473
|
/** What the sync's events leave out, said where they are listed (mason logs --help, the page's Sync events, held equal
|
|
1314
1474
|
* by tests/web-page.test.ts): a scheduled poll that succeeds is no event, so a person who synced reads where it is. */
|
|
@@ -1357,8 +1517,9 @@ const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<
|
|
|
1357
1517
|
--running the statements running now (Mason must be running)
|
|
1358
1518
|
--json as data: { lines: [{ at, text }] } (the log's entries, oldest first; at is null for a line with no
|
|
1359
1519
|
time); with --requests { requests: [{ requestId, startedAt, endedAt, session, applicationName,
|
|
1360
|
-
|
|
1361
|
-
|
|
1520
|
+
clientAddress (the client's address; behind a TCP proxy, the one its PROXY header carries),
|
|
1521
|
+
tokenName, user (the mason user), role, attributes (the user's values its role's row filters
|
|
1522
|
+
read, or null), method, outcome (ok, refused, failed, cancelled), status
|
|
1362
1523
|
(completed, failed, cancelled: what a reader shows), sqlstate, message, detail, hint, rows,
|
|
1363
1524
|
bytesSent, durationMs, query, queryTruncated }], keptDays, before (the position of the page before,
|
|
1364
1525
|
or null: none older) }; with --events { events: [{ at, kind (copy_failed, poll_failed,
|
|
@@ -1572,16 +1733,22 @@ export async function logs(argv) {
|
|
|
1572
1733
|
return failed("logs", t, { json });
|
|
1573
1734
|
// the run's log by default (mason logs once showed no actual logs: it showed
|
|
1574
1735
|
// mason start's terminal, which after the start carries only warnings and errors)
|
|
1575
|
-
const file =
|
|
1736
|
+
const file = logPath(t.project.dir, f.values.start ? "start" : "run");
|
|
1576
1737
|
if (!fs.existsSync(file)) {
|
|
1577
1738
|
return failed("logs", { message: `${file} does not exist yet`, code: "nothing_yet", why: `Mason has not run for ${t.project.config.name}`, next: [["mason start", "start it, in the background"]] }, { json });
|
|
1578
1739
|
}
|
|
1579
|
-
|
|
1740
|
+
// read through the one reader, every line masked as it is read (src/lib/log-read.ts, issue 621: an older Mason's lines
|
|
1741
|
+
// held the source's password in a `password=` value); a line not yet ended is -f's to show as it stands, else read whole
|
|
1742
|
+
const reader = new LogReader(file);
|
|
1743
|
+
const read = reader.readLines();
|
|
1744
|
+
const unended = f.values.follow ? "" : reader.rest();
|
|
1745
|
+
if (unended !== "")
|
|
1746
|
+
read.push(unended);
|
|
1580
1747
|
// WHY THE FILE, even with the run up: it is the whole record (the run's memory keeps only its last LOG_KEPT entries,
|
|
1581
1748
|
// src/lib/log-buffer.ts) and the only one when the run is down. The entries are parsed by the buffer's own parser, so
|
|
1582
1749
|
// `{ at, text }` is the shape of GET /api/logs too (the run's answer, which a `mason serve` run has no file for).
|
|
1583
1750
|
// -n counts entries, a multi-line one being ONE; dev.log's lines (the start's steps, indented under each other) are a line each.
|
|
1584
|
-
const all =
|
|
1751
|
+
const all = entriesOfLines(read, { perLine: f.values.start === true });
|
|
1585
1752
|
const last = all.slice(Math.max(0, all.length - lines));
|
|
1586
1753
|
if (json) {
|
|
1587
1754
|
out(jsonText(LOGS, { lines: last }, [["mason logs --requests", "every statement the Postgres API met"]]));
|
|
@@ -1591,24 +1758,13 @@ export async function logs(argv) {
|
|
|
1591
1758
|
out(logLineText(l));
|
|
1592
1759
|
if (!f.values.follow)
|
|
1593
1760
|
return 0;
|
|
1594
|
-
let offset = Buffer.byteLength(text);
|
|
1595
1761
|
for (;;) {
|
|
1762
|
+
// the lines written since, each masked once it is whole, then the line not ended yet as it stands, masked (a run
|
|
1763
|
+
// killed mid-write leaves its last words unended); a new run that started the file over is read from its start
|
|
1764
|
+
const more = reader.follow();
|
|
1765
|
+
if (more !== "")
|
|
1766
|
+
process.stdout.write(more);
|
|
1596
1767
|
await new Promise((res) => setTimeout(res, 500));
|
|
1597
|
-
const size = fs.statSync(file).size;
|
|
1598
|
-
if (size < offset)
|
|
1599
|
-
offset = 0; // a new run started the file over
|
|
1600
|
-
if (size === offset)
|
|
1601
|
-
continue;
|
|
1602
|
-
const buf = Buffer.alloc(size - offset);
|
|
1603
|
-
const fd = fs.openSync(file, "r");
|
|
1604
|
-
try {
|
|
1605
|
-
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
1606
|
-
}
|
|
1607
|
-
finally {
|
|
1608
|
-
fs.closeSync(fd);
|
|
1609
|
-
}
|
|
1610
|
-
offset = size;
|
|
1611
|
-
process.stdout.write(buf.toString());
|
|
1612
1768
|
}
|
|
1613
1769
|
}
|
|
1614
1770
|
// ------------------------------------------------------------------ mason stop
|