@steve31415/baselib 3.38.1 → 3.39.1
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/bin/pw-deploy.js +6 -4
- package/dist/bin/pw-logs.js +2 -1
- package/dist/bin/pw-rollback.js +2 -1
- package/dist/bin/pw-sql.js +2 -1
- package/dist/bin/release.js +2 -1
- package/dist/ops/cli.d.ts +12 -0
- package/dist/ops/cli.js +26 -0
- package/dist/sync/engine.js +4 -0
- package/dist/sync-browser/client.d.ts +5 -0
- package/dist/sync-browser/client.js +42 -4
- package/package.json +1 -1
package/dist/bin/pw-deploy.js
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
// ~/migration/research/deploy-procedure-design.md). All logic lives in
|
|
4
4
|
// ../deploy/deploy.ts; configuration is the app's deploy.config.json.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// a failed deploy's process alive; the server-side build finishes on its
|
|
8
|
-
// and is reused by the next attempt.
|
|
6
|
+
// An explicit exit (not exitCode) so an in-flight Cloud Build child cannot
|
|
7
|
+
// keep a failed deploy's process alive; the server-side build finishes on its
|
|
8
|
+
// own and is reused by the next attempt. exitWhenFlushed lets the output
|
|
9
|
+
// already written reach a piped reader (Coder captures it) first.
|
|
10
|
+
import { exitWhenFlushed } from '../ops/cli.js';
|
|
9
11
|
import { realRunner } from '../deploy/exec.js';
|
|
10
12
|
import { runDeploy } from '../deploy/deploy.js';
|
|
11
|
-
|
|
13
|
+
await exitWhenFlushed(await runDeploy({
|
|
12
14
|
repoRoot: process.cwd(),
|
|
13
15
|
runner: realRunner,
|
|
14
16
|
argv: process.argv.slice(2),
|
package/dist/bin/pw-logs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `pw-logs` — Axiom log query (OPERATIONS.md "Investigating production
|
|
3
3
|
// state"). All logic lives in ../ops/logs.ts.
|
|
4
|
+
import { exitWhenFlushed } from '../ops/cli.js';
|
|
4
5
|
import { runLogs } from '../ops/logs.js';
|
|
5
|
-
|
|
6
|
+
await exitWhenFlushed(await runLogs({ argv: process.argv.slice(2), cwd: process.cwd() }));
|
package/dist/bin/pw-rollback.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `pw-rollback` — one-command traffic move to a previously released build.
|
|
3
3
|
// All logic lives in ../deploy/rollback.ts.
|
|
4
|
+
import { exitWhenFlushed } from '../ops/cli.js';
|
|
4
5
|
import { realRunner } from '../deploy/exec.js';
|
|
5
6
|
import { runRollback } from '../deploy/rollback.js';
|
|
6
|
-
|
|
7
|
+
await exitWhenFlushed(await runRollback({
|
|
7
8
|
repoRoot: process.cwd(),
|
|
8
9
|
runner: realRunner,
|
|
9
10
|
argv: process.argv.slice(2),
|
package/dist/bin/pw-sql.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `pw-sql` — read-only production SQL (OPERATIONS.md "Investigating
|
|
3
3
|
// production state"). All logic lives in ../ops/sql.ts.
|
|
4
|
+
import { exitWhenFlushed } from '../ops/cli.js';
|
|
4
5
|
import { runSql } from '../ops/sql.js';
|
|
5
|
-
|
|
6
|
+
await exitWhenFlushed(await runSql({ argv: process.argv.slice(2), cwd: process.cwd() }));
|
package/dist/bin/release.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// baselib's `npm run deploy` — release the checked-out version (tag push +
|
|
3
3
|
// wait for the registry; the tag-triggered workflow does the actual
|
|
4
4
|
// publish). All logic lives in ../release.ts.
|
|
5
|
+
import { exitWhenFlushed } from '../ops/cli.js';
|
|
5
6
|
import { realRunner } from '../deploy/exec.js';
|
|
6
7
|
import { release } from '../release.js';
|
|
7
8
|
try {
|
|
@@ -12,5 +13,5 @@ try {
|
|
|
12
13
|
}
|
|
13
14
|
catch (e) {
|
|
14
15
|
console.error(e instanceof Error ? e.message : String(e));
|
|
15
|
-
|
|
16
|
+
await exitWhenFlushed(1);
|
|
16
17
|
}
|
package/dist/ops/cli.d.ts
CHANGED
|
@@ -13,4 +13,16 @@ type Parsed<T extends Options> = ReturnType<typeof parseArgs<{
|
|
|
13
13
|
export declare function parseCli<T extends Options>(argv: string[], options: T, usage: string, io: CliIo): Parsed<T> | {
|
|
14
14
|
code: number;
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* `process.exit(code)` once stdout and stderr have handed everything written
|
|
18
|
+
* to them to the OS. A plain process.exit discards what a pipe has not yet
|
|
19
|
+
* taken: Node writes to a pipe asynchronously once its buffer (64 KiB on
|
|
20
|
+
* Linux) is full, so `pw-logs … | jq` lost all but the first 64 KiB, with
|
|
21
|
+
* exit status 0 (2026-10-26). Files and terminals are written synchronously,
|
|
22
|
+
* which is why redirecting to a file never showed it. Still an explicit exit,
|
|
23
|
+
* not `exitCode`: nothing else the bin started (a connection pool, a Cloud
|
|
24
|
+
* Build child) may keep it alive. A reader that goes away (`| head`) ends the
|
|
25
|
+
* wait instead of crashing the bin on EPIPE.
|
|
26
|
+
*/
|
|
27
|
+
export declare function exitWhenFlushed(code: number, streams?: NodeJS.WritableStream[], exit?: (code: number) => void): Promise<void>;
|
|
16
28
|
export {};
|
package/dist/ops/cli.js
CHANGED
|
@@ -25,3 +25,29 @@ export function parseCli(argv, options, usage, io) {
|
|
|
25
25
|
}
|
|
26
26
|
return parsed;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* `process.exit(code)` once stdout and stderr have handed everything written
|
|
30
|
+
* to them to the OS. A plain process.exit discards what a pipe has not yet
|
|
31
|
+
* taken: Node writes to a pipe asynchronously once its buffer (64 KiB on
|
|
32
|
+
* Linux) is full, so `pw-logs … | jq` lost all but the first 64 KiB, with
|
|
33
|
+
* exit status 0 (2026-10-26). Files and terminals are written synchronously,
|
|
34
|
+
* which is why redirecting to a file never showed it. Still an explicit exit,
|
|
35
|
+
* not `exitCode`: nothing else the bin started (a connection pool, a Cloud
|
|
36
|
+
* Build child) may keep it alive. A reader that goes away (`| head`) ends the
|
|
37
|
+
* wait instead of crashing the bin on EPIPE.
|
|
38
|
+
*/
|
|
39
|
+
export async function exitWhenFlushed(code, streams = [process.stdout, process.stderr], exit = (c) => process.exit(c)) {
|
|
40
|
+
await Promise.all(streams.map(flushed));
|
|
41
|
+
exit(code);
|
|
42
|
+
}
|
|
43
|
+
function flushed(stream) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
// A broken pipe is reported to the write callback and then, a tick
|
|
46
|
+
// later, as an 'error' event, which would crash the process if nothing
|
|
47
|
+
// listened — so the listener stays until the exit.
|
|
48
|
+
stream.on('error', () => resolve());
|
|
49
|
+
// Writes complete in order, so an empty write's callback runs only after
|
|
50
|
+
// everything written before it has gone out.
|
|
51
|
+
stream.write('', () => resolve());
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/sync/engine.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { SyncError, } from './types.js';
|
|
3
|
+
// Past this many events a client is told to reload (a full bootstrap)
|
|
4
|
+
// rather than catch up. Deliberate (2026-10-20, task 1e2cd274): a todo page
|
|
5
|
+
// 138 events behind resynced as designed; the fault was its cached, stale
|
|
6
|
+
// bootstrap, fixed in the app (no-store), not this limit.
|
|
3
7
|
const DEFAULT_CATCHUP_LIMIT = 100;
|
|
4
8
|
const DEFAULT_RETENTION_DAYS = 7;
|
|
5
9
|
const IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
|
|
@@ -154,6 +154,9 @@ export declare class SyncClient<B, C extends EventBody, S extends EventBody, Sum
|
|
|
154
154
|
* after the bootstrap installs. Non-null exactly while reconciling. */
|
|
155
155
|
private gapBuffer;
|
|
156
156
|
private resyncBackoffTimer;
|
|
157
|
+
/** The backstop retry after a failed resync. Unlike the loop backoff it
|
|
158
|
+
* absorbs nothing: a fresh reload request runs at once and cancels it. */
|
|
159
|
+
private resyncFailureTimer;
|
|
157
160
|
private loopEscalated;
|
|
158
161
|
private reconciliationTail;
|
|
159
162
|
private initialization;
|
|
@@ -302,6 +305,8 @@ export declare class SyncClient<B, C extends EventBody, S extends EventBody, Sum
|
|
|
302
305
|
* cycle every reconcile succeeds. */
|
|
303
306
|
private endResyncEpisode;
|
|
304
307
|
private scheduleResyncRetry;
|
|
308
|
+
private scheduleResyncFailureRetry;
|
|
309
|
+
private cancelResyncFailureRetry;
|
|
305
310
|
private sendConnect;
|
|
306
311
|
private sendRaw;
|
|
307
312
|
private trySend;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { FirstLoadRetry } from './first-load-retry.js';
|
|
2
2
|
const RECONNECT_DELAYS_SECONDS = [1, 2, 4, 8, 16, 30];
|
|
3
3
|
const PING_INTERVAL_MS = 30_000;
|
|
4
|
+
// Deliberate (2026-10-20 review, task 1e2cd274): todo's phone hit it 27
|
|
5
|
+
// times in 25 minutes on 10-19, but none of those handshakes ever reached
|
|
6
|
+
// the load balancer, so a longer limit would only have delayed the retry.
|
|
4
7
|
const CONNECT_WATCHDOG_MS = 10_000;
|
|
5
8
|
const DEFAULT_RECONNECT_BEFORE_MS = 55 * 60 * 1000;
|
|
6
9
|
const FAILURE_TIMEOUT_MS = 60_000;
|
|
@@ -89,6 +92,9 @@ export class SyncClient {
|
|
|
89
92
|
* after the bootstrap installs. Non-null exactly while reconciling. */
|
|
90
93
|
gapBuffer = null;
|
|
91
94
|
resyncBackoffTimer = null;
|
|
95
|
+
/** The backstop retry after a failed resync. Unlike the loop backoff it
|
|
96
|
+
* absorbs nothing: a fresh reload request runs at once and cancels it. */
|
|
97
|
+
resyncFailureTimer = null;
|
|
92
98
|
loopEscalated = false;
|
|
93
99
|
reconciliationTail = Promise.resolve();
|
|
94
100
|
initialization = null;
|
|
@@ -647,7 +653,8 @@ export class SyncClient {
|
|
|
647
653
|
}
|
|
648
654
|
const planned = this.plannedClose === socket;
|
|
649
655
|
this.plannedClose = null;
|
|
650
|
-
this.
|
|
656
|
+
const openForMs = this.socketOpenedAt === null ? null : Math.max(0, this.now() - this.socketOpenedAt);
|
|
657
|
+
this.startOutage(event.code, event.wasClean === true, openForMs);
|
|
651
658
|
if (!this.isHidden()) {
|
|
652
659
|
this.noteReturnFailure(event.code);
|
|
653
660
|
this.scheduleReconnect();
|
|
@@ -972,7 +979,7 @@ export class SyncClient {
|
|
|
972
979
|
void this.probeSession();
|
|
973
980
|
}
|
|
974
981
|
}
|
|
975
|
-
startOutage(closeCode, wasClean = false) {
|
|
982
|
+
startOutage(closeCode, wasClean = false, openForMs = null) {
|
|
976
983
|
if (this.outageStartedAt !== null)
|
|
977
984
|
return;
|
|
978
985
|
// A sleep or freeze that ended in this close happened before the outage
|
|
@@ -982,7 +989,7 @@ export class SyncClient {
|
|
|
982
989
|
this.outageStartedAt = this.options.runtime.now();
|
|
983
990
|
this.outageEscalated = false;
|
|
984
991
|
this.outageCloseCode = closeCode;
|
|
985
|
-
this.outageInfo = { closeCode, wasClean, visibleAtDrop: !this.isHidden(), counters: this.snapshot() };
|
|
992
|
+
this.outageInfo = { closeCode, wasClean, openForMs, visibleAtDrop: !this.isHidden(), counters: this.snapshot() };
|
|
986
993
|
this.armEscalation();
|
|
987
994
|
}
|
|
988
995
|
/** ERROR only after OUTAGE_ESCALATE_MS of visible, on-time outage; re-armed
|
|
@@ -1002,6 +1009,7 @@ export class SyncClient {
|
|
|
1002
1009
|
durationMs: this.options.runtime.now() - this.outageStartedAt,
|
|
1003
1010
|
closeCode: this.outageCloseCode,
|
|
1004
1011
|
attempts: this.reconnectAttempts,
|
|
1012
|
+
openForMs: this.outageInfo?.openForMs ?? null,
|
|
1005
1013
|
});
|
|
1006
1014
|
}
|
|
1007
1015
|
}, OUTAGE_ESCALATE_MS);
|
|
@@ -1327,6 +1335,7 @@ export class SyncClient {
|
|
|
1327
1335
|
totalAttempts: this.totalAttempts,
|
|
1328
1336
|
closeCode: info?.closeCode,
|
|
1329
1337
|
wasClean: info?.wasClean,
|
|
1338
|
+
openForMs: info ? info.openForMs : undefined,
|
|
1330
1339
|
visibleAtDrop: info?.visibleAtDrop,
|
|
1331
1340
|
hiddenMs: delta?.hiddenMs,
|
|
1332
1341
|
stalledMs: delta?.stalledMs,
|
|
@@ -1387,6 +1396,7 @@ export class SyncClient {
|
|
|
1387
1396
|
async resyncFromServer(reason) {
|
|
1388
1397
|
if (this.refetchInFlight || this.halted || this.disposed)
|
|
1389
1398
|
return;
|
|
1399
|
+
this.cancelResyncFailureRetry();
|
|
1390
1400
|
const now = this.options.runtime.now();
|
|
1391
1401
|
// `time <= now` drops entries stranded in the future by a backward
|
|
1392
1402
|
// clock jump, which would otherwise pin the detector tripped.
|
|
@@ -1420,7 +1430,17 @@ export class SyncClient {
|
|
|
1420
1430
|
}
|
|
1421
1431
|
catch (error) {
|
|
1422
1432
|
if (!isLifecycleAbort(error) && !this.disposed && !this.halted) {
|
|
1423
|
-
|
|
1433
|
+
// As for the first bootstrap: fetch's TypeError means no HTTP answer
|
|
1434
|
+
// reached the page (the device's network), a WARN; an answer that was
|
|
1435
|
+
// an error is the server's, an ERROR. Either way the store may be
|
|
1436
|
+
// stale, so a retry is scheduled rather than left to the next reload
|
|
1437
|
+
// message or sequence gap, which a quiet page may never get.
|
|
1438
|
+
const reachedServer = !(error instanceof TypeError);
|
|
1439
|
+
if (reachedServer)
|
|
1440
|
+
this.options.logger.error('resync failed', { reason, reachedServer, error });
|
|
1441
|
+
else
|
|
1442
|
+
this.options.logger.warn('resync failed', { reason, reachedServer, error });
|
|
1443
|
+
this.scheduleResyncFailureRetry();
|
|
1424
1444
|
}
|
|
1425
1445
|
}
|
|
1426
1446
|
finally {
|
|
@@ -1472,6 +1492,23 @@ export class SyncClient {
|
|
|
1472
1492
|
void this.resyncFromServer('retry after resync backoff');
|
|
1473
1493
|
}, RESYNC_RETRY_MS);
|
|
1474
1494
|
}
|
|
1495
|
+
scheduleResyncFailureRetry() {
|
|
1496
|
+
if (this.resyncFailureTimer !== null || this.halted || this.disposed)
|
|
1497
|
+
return;
|
|
1498
|
+
this.resyncFailureTimer = this.setTimer(() => {
|
|
1499
|
+
this.resyncFailureTimer = null;
|
|
1500
|
+
// Hidden: no minutely retry (a phone without network would log one a
|
|
1501
|
+
// minute); the return's HTTP catch-up re-triggers the resync.
|
|
1502
|
+
if (this.isHidden())
|
|
1503
|
+
return;
|
|
1504
|
+
void this.resyncFromServer('retry after failed resync');
|
|
1505
|
+
}, RESYNC_RETRY_MS);
|
|
1506
|
+
}
|
|
1507
|
+
cancelResyncFailureRetry() {
|
|
1508
|
+
if (this.resyncFailureTimer !== null)
|
|
1509
|
+
this.options.runtime.clearTimeout(this.resyncFailureTimer);
|
|
1510
|
+
this.resyncFailureTimer = null;
|
|
1511
|
+
}
|
|
1475
1512
|
sendConnect() {
|
|
1476
1513
|
this.sendRaw({
|
|
1477
1514
|
type: 'connect',
|
|
@@ -1600,6 +1637,7 @@ export class SyncClient {
|
|
|
1600
1637
|
this.options.runtime.clearTimeout(this.escalateTimer);
|
|
1601
1638
|
if (this.resyncBackoffTimer !== null)
|
|
1602
1639
|
this.options.runtime.clearTimeout(this.resyncBackoffTimer);
|
|
1640
|
+
this.cancelResyncFailureRetry();
|
|
1603
1641
|
for (const timer of this.failureTimers.values())
|
|
1604
1642
|
this.options.runtime.clearTimeout(timer);
|
|
1605
1643
|
this.cancelBackgroundTick();
|
package/package.json
CHANGED