@spooky-sync/core 0.0.1-canary.32 → 0.0.1-canary.321
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/AGENTS.md +81 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2034 -137
- package/dist/index.js +15236 -2467
- package/dist/live.d.ts +141 -0
- package/dist/live.js +733 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +365 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +379 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +439 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +472 -0
- package/dist/types.d.ts +1821 -38
- package/dist/worker-edges.js +470 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +48 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +27 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +172 -0
- package/skills/sp00ky-core/SKILL.md +31 -6
- package/skills/sp00ky-core/references/config.md +6 -0
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +180 -0
- package/src/boot/boot.saga.ts +142 -0
- package/src/boot/bucket-switch.saga.test.ts +187 -0
- package/src/boot/bucket-switch.saga.ts +133 -0
- package/src/boot/preload.saga.test.ts +51 -0
- package/src/boot/preload.saga.ts +31 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +117 -0
- package/src/client/bucket-handle.ts +227 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +60 -0
- package/src/client/services.ts +418 -0
- package/src/client/sp00ky-client.test.ts +303 -0
- package/src/client/sp00ky-client.ts +586 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +69 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +80 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +163 -0
- package/src/kernel/events.ts +57 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +148 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/live/index.test.ts +501 -0
- package/src/live/index.ts +1039 -0
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +209 -0
- package/src/modules/auth/auth.local-first.test.ts +169 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +466 -19
- package/src/modules/crdt/crdt-field.ts +294 -0
- package/src/modules/crdt/crdt-hydration.test.ts +210 -0
- package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
- package/src/modules/crdt/index.ts +463 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +93 -0
- package/src/modules/ref-tables.ts +91 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +285 -0
- package/src/mutation/push.saga.ts +217 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +337 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +56 -0
- package/src/query/fetch.saga.test.ts +199 -0
- package/src/query/fetch.saga.ts +120 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/query/id-set-plan.test.ts +132 -0
- package/src/query/lifecycle.saga.test.ts +274 -0
- package/src/query/lifecycle.saga.ts +159 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +350 -0
- package/src/query/membership.saga.ts +278 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +229 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +112 -0
- package/src/query/sql.ts +149 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +195 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +199 -0
- package/src/services/database/connection-supervisor.test.ts +318 -0
- package/src/services/database/connection-supervisor.ts +422 -0
- package/src/services/database/database.query-timeout.test.ts +140 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +146 -61
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/landed-body-clear.integration.test.ts +236 -0
- package/src/services/database/local-migrator.integration.test.ts +110 -0
- package/src/services/database/local-migrator.ts +59 -16
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +487 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +273 -6
- package/src/services/database/sqlite-cache-engine.test.ts +617 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1372 -0
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
- package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
- package/src/services/database/sqlite-lock-verify.test.ts +33 -0
- package/src/services/database/sqlite-lock-verify.ts +45 -0
- package/src/services/database/sqlite-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +145 -0
- package/src/services/database/sqlite-plan-sql.ts +182 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +416 -0
- package/src/services/database/surreal-cache-engine.ts +161 -0
- package/src/services/logger/index.test.ts +132 -0
- package/src/services/logger/index.ts +189 -7
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +10 -3
- package/src/services/persistence/surrealdb.ts +4 -4
- package/src/services/stream-processor/index.ts +783 -68
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +58 -2
- package/src/services/tabs/broker-client.ts +283 -0
- package/src/services/tabs/broker.test.ts +327 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +638 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +262 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +640 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +200 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +304 -0
- package/src/state/reducers.ts +526 -0
- package/src/state/selectors.test.ts +175 -0
- package/src/state/selectors.ts +222 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +97 -0
- package/src/sync/connection.saga.ts +89 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +663 -0
- package/src/sync/policy.ts +515 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +133 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +48 -0
- package/src/testing/run-pure.test.ts +92 -0
- package/src/testing/run-pure.ts +128 -0
- package/src/testing/services.ts +17 -0
- package/src/types.ts +624 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/error-classification.test.ts +54 -0
- package/src/utils/error-classification.ts +25 -0
- package/src/utils/index.ts +78 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +30 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -167
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { LogTap, createLogger, stringifyLog } from './index';
|
|
3
|
+
|
|
4
|
+
// Vitest runs in node, where `pino` resolves to its node build and ignores the
|
|
5
|
+
// `browser` options the tap hangs off. Apps bundle the browser build.
|
|
6
|
+
// The specifier is widened to `string`: pino ships no types for that entry.
|
|
7
|
+
vi.mock('pino', async () => ({ default: (await import('pino/browser.js' as string)).default }));
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
vi.unstubAllGlobals();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function silentConsole() {
|
|
15
|
+
return vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('LogTap', () => {
|
|
19
|
+
it('records what the logger writes, child bindings included', () => {
|
|
20
|
+
silentConsole();
|
|
21
|
+
const tap = new LogTap('info');
|
|
22
|
+
const logger = createLogger('info', undefined, tap);
|
|
23
|
+
logger.child({ service: 'Database' }).warn({ Category: 'x' }, 'slow query');
|
|
24
|
+
logger.debug('not at this level');
|
|
25
|
+
const { entries, head } = tap.read();
|
|
26
|
+
expect(entries).toHaveLength(1);
|
|
27
|
+
expect(head).toBe(1);
|
|
28
|
+
expect(entries[0]).toMatchObject({ seq: 1, level: 40, msg: 'slow query' });
|
|
29
|
+
expect(JSON.parse(entries[0].line)).toMatchObject({ service: 'Database', Category: 'x' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('captures below the console level without printing it, on existing children too', () => {
|
|
33
|
+
const printed = silentConsole();
|
|
34
|
+
const tap = new LogTap('info');
|
|
35
|
+
const logger = createLogger('info', undefined, tap);
|
|
36
|
+
const child = logger.child({ service: 'Sync' });
|
|
37
|
+
child.debug('before');
|
|
38
|
+
tap.setCaptureLevel('debug');
|
|
39
|
+
child.debug('after');
|
|
40
|
+
logger.info('shown');
|
|
41
|
+
expect(tap.read().entries.map((e) => e.msg)).toEqual(['after', 'shown']);
|
|
42
|
+
expect(printed).toHaveBeenCalledTimes(1);
|
|
43
|
+
expect(String(printed.mock.calls[0][0])).toContain('shown');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('keeps the capture level for the tab session', () => {
|
|
47
|
+
const store = new Map<string, string>();
|
|
48
|
+
vi.stubGlobal('sessionStorage', {
|
|
49
|
+
getItem: (k: string) => store.get(k) ?? null,
|
|
50
|
+
setItem: (k: string, v: string) => void store.set(k, v),
|
|
51
|
+
removeItem: (k: string) => void store.delete(k),
|
|
52
|
+
});
|
|
53
|
+
new LogTap('info').setCaptureLevel('trace');
|
|
54
|
+
expect(new LogTap('info').captureLevel).toBe('trace');
|
|
55
|
+
new LogTap('info').setCaptureLevel('info');
|
|
56
|
+
expect(store.size).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('is a bounded ring that reports what a slow reader missed', () => {
|
|
60
|
+
silentConsole();
|
|
61
|
+
const tap = new LogTap('info');
|
|
62
|
+
const logger = createLogger('info', undefined, tap);
|
|
63
|
+
for (let i = 0; i < LogTap.CAPACITY + 10; i++) logger.info(`line ${i}`);
|
|
64
|
+
const all = tap.read();
|
|
65
|
+
expect(all.entries).toHaveLength(LogTap.CAPACITY);
|
|
66
|
+
expect(all.dropped).toBe(10);
|
|
67
|
+
const delta = tap.read(all.head - 2);
|
|
68
|
+
expect(delta.entries.map((e) => e.msg)).toEqual([`line ${LogTap.CAPACITY + 8}`, `line ${LogTap.CAPACITY + 9}`]);
|
|
69
|
+
expect(delta.dropped).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('does not count cleared lines as dropped', () => {
|
|
73
|
+
silentConsole();
|
|
74
|
+
const tap = new LogTap('info');
|
|
75
|
+
const logger = createLogger('info', undefined, tap);
|
|
76
|
+
logger.info('a');
|
|
77
|
+
logger.info('b');
|
|
78
|
+
tap.clear();
|
|
79
|
+
logger.info('c');
|
|
80
|
+
expect(tap.read(0)).toMatchObject({ dropped: 0, head: 3 });
|
|
81
|
+
expect(tap.read(0).entries.map((e) => e.msg)).toEqual(['c']);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('caps a huge line', () => {
|
|
85
|
+
silentConsole();
|
|
86
|
+
const tap = new LogTap('info');
|
|
87
|
+
createLogger('info', undefined, tap).info({ blob: 'x'.repeat(LogTap.LINE_MAX * 2) }, 'big');
|
|
88
|
+
const [entry] = tap.read().entries;
|
|
89
|
+
expect(entry.line.length).toBe(LogTap.LINE_MAX + 1);
|
|
90
|
+
expect(entry.msg).toBe('big');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('notifies listeners and survives one that throws', () => {
|
|
94
|
+
silentConsole();
|
|
95
|
+
const tap = new LogTap('info');
|
|
96
|
+
const logger = createLogger('info', undefined, tap);
|
|
97
|
+
const seen: number[] = [];
|
|
98
|
+
tap.subscribe(() => {
|
|
99
|
+
throw new Error('listener bug');
|
|
100
|
+
});
|
|
101
|
+
tap.subscribe(() => seen.push(tap.head));
|
|
102
|
+
expect(() => logger.info('x')).not.toThrow();
|
|
103
|
+
expect(seen).toEqual([1]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('keeps the otel transmit at the configured level when capture goes lower', () => {
|
|
107
|
+
silentConsole();
|
|
108
|
+
const sent: string[] = [];
|
|
109
|
+
const tap = new LogTap('info');
|
|
110
|
+
const logger = createLogger('info', { send: (level) => void sent.push(level) }, tap);
|
|
111
|
+
tap.setCaptureLevel('debug');
|
|
112
|
+
logger.debug('d');
|
|
113
|
+
logger.info('i');
|
|
114
|
+
expect(sent).toEqual(['info']);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('stringifyLog', () => {
|
|
119
|
+
it('keeps errors readable and never throws on bigints or cycles', () => {
|
|
120
|
+
const o: Record<string, unknown> = { err: new Error('boom'), n: 10n };
|
|
121
|
+
o.self = o;
|
|
122
|
+
const parsed = JSON.parse(stringifyLog(o));
|
|
123
|
+
expect(parsed.err).toMatchObject({ type: 'Error', message: 'boom' });
|
|
124
|
+
expect(parsed.n).toBe('10');
|
|
125
|
+
expect(parsed.self).toBe('[Circular]');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('does not mistake a shared reference for a cycle', () => {
|
|
129
|
+
const shared = { a: 1 };
|
|
130
|
+
expect(JSON.parse(stringifyLog({ x: shared, y: shared }))).toEqual({ x: { a: 1 }, y: { a: 1 } });
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -1,22 +1,204 @@
|
|
|
1
|
-
import pino, { Level, type Logger as PinoLogger, type LoggerOptions } from 'pino';
|
|
2
|
-
import { PinoTransmit } from '../../types';
|
|
1
|
+
import pino, { type Level, type Logger as PinoLogger, type LoggerOptions } from 'pino';
|
|
2
|
+
import type { PinoTransmit } from '../../types';
|
|
3
3
|
|
|
4
4
|
export type Logger = PinoLogger;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** One line the logger wrote, as the DevTools Logs tab and the MCP read it. */
|
|
7
|
+
export interface CapturedLog {
|
|
8
|
+
/** Monotonic for the page's lifetime; a reader resumes after the last one it holds. */
|
|
9
|
+
seq: number;
|
|
10
|
+
time: number;
|
|
11
|
+
/** Pino's numeric level: 10 trace … 60 fatal. */
|
|
12
|
+
level: number;
|
|
13
|
+
msg: string;
|
|
14
|
+
/** The whole log object as JSON (what the console printed), capped at {@link LogTap.LINE_MAX}. */
|
|
15
|
+
line: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LogRead {
|
|
19
|
+
entries: CapturedLog[];
|
|
20
|
+
/** `seq` of the newest captured line; pass it back as `after` to resume. */
|
|
21
|
+
head: number;
|
|
22
|
+
/** Lines after `after` the ring overwrote before they were read (cleared ones excluded). */
|
|
23
|
+
dropped: number;
|
|
24
|
+
consoleLevel: Level;
|
|
25
|
+
captureLevel: Level;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const LEVELS = pino.levels.values as Record<Level, number>;
|
|
29
|
+
const CAPTURE_KEY = 'sp00ky:devtools:logCapture';
|
|
30
|
+
|
|
31
|
+
function isLevel(value: unknown): value is Level {
|
|
32
|
+
return typeof value === 'string' && value in LEVELS;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* JSON for a log object that can never throw into the call site: Errors keep
|
|
37
|
+
* their message and stack (plain `JSON.stringify` prints `{}`), bigints become
|
|
38
|
+
* strings, and a cycle prints `[Circular]` instead of throwing.
|
|
39
|
+
*/
|
|
40
|
+
export function stringifyLog(o: unknown): string {
|
|
41
|
+
const ancestors: unknown[] = [];
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(o, function (this: unknown, _key: string, value: unknown) {
|
|
44
|
+
if (typeof value === 'bigint') return value.toString();
|
|
45
|
+
if (value instanceof Error) return { type: value.name, message: value.message, stack: value.stack };
|
|
46
|
+
if (typeof value !== 'object' || value === null) return value;
|
|
47
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop();
|
|
48
|
+
if (ancestors.includes(value)) return '[Circular]';
|
|
49
|
+
ancestors.push(value);
|
|
50
|
+
return value;
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
const msg = (o as { msg?: unknown } | null)?.msg;
|
|
54
|
+
return JSON.stringify({ msg: typeof msg === 'string' ? msg : '[unserializable log object]' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The page-side log buffer behind the DevTools Logs tab. Always on and
|
|
60
|
+
* bounded: a line costs the JSON the console write produces anyway, and a
|
|
61
|
+
* panel opened after boot still sees the boot.
|
|
62
|
+
*
|
|
63
|
+
* It also owns the logger's level. The console prints at the configured level
|
|
64
|
+
* only; `setCaptureLevel` changes what is RECORDED (debug without flooding the
|
|
65
|
+
* console), live on every child logger, and keeps it for this tab's session so
|
|
66
|
+
* a reload captures its boot at that level too.
|
|
67
|
+
*/
|
|
68
|
+
export class LogTap {
|
|
69
|
+
static readonly CAPACITY = 500;
|
|
70
|
+
static readonly LINE_MAX = 4000;
|
|
71
|
+
|
|
72
|
+
private readonly ring: CapturedLog[] = [];
|
|
73
|
+
private seq = 0;
|
|
74
|
+
/** `seq` at the last `clear()`: lines up to here are gone on purpose, not dropped. */
|
|
75
|
+
private clearedThrough = 0;
|
|
76
|
+
private readonly listeners = new Set<() => void>();
|
|
77
|
+
private readonly loggers: Array<WeakRef<Logger>> = [];
|
|
78
|
+
private captureLevelValue: Level;
|
|
79
|
+
|
|
80
|
+
constructor(readonly consoleLevel: Level) {
|
|
81
|
+
this.captureLevelValue = consoleLevel;
|
|
82
|
+
try {
|
|
83
|
+
const saved = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(CAPTURE_KEY) : null;
|
|
84
|
+
if (isLevel(saved)) this.captureLevelValue = saved;
|
|
85
|
+
} catch {
|
|
86
|
+
// Storage blocked: capture at the console level.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `seq` of the newest captured line. */
|
|
91
|
+
get head(): number {
|
|
92
|
+
return this.seq;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get captureLevel(): Level {
|
|
96
|
+
return this.captureLevelValue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What pino has to produce for both the console and the buffer. */
|
|
100
|
+
get effectiveLevel(): Level {
|
|
101
|
+
return LEVELS[this.captureLevelValue] < LEVELS[this.consoleLevel] ? this.captureLevelValue : this.consoleLevel;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether the console prints a line at this numeric level. */
|
|
105
|
+
printsToConsole(level: number): boolean {
|
|
106
|
+
return level >= LEVELS[this.consoleLevel];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Remember a logger (the root and every child) so a level change reaches it. */
|
|
110
|
+
track(logger: Logger): void {
|
|
111
|
+
this.loggers.push(new WeakRef(logger));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setCaptureLevel(level: Level): void {
|
|
115
|
+
if (!isLevel(level)) throw new Error(`unknown log level: ${String(level)}`);
|
|
116
|
+
this.captureLevelValue = level;
|
|
117
|
+
try {
|
|
118
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
119
|
+
if (level === this.consoleLevel) sessionStorage.removeItem(CAPTURE_KEY);
|
|
120
|
+
else sessionStorage.setItem(CAPTURE_KEY, level);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Storage blocked: the change holds until the page goes away.
|
|
124
|
+
}
|
|
125
|
+
const effective = this.effectiveLevel;
|
|
126
|
+
for (let i = this.loggers.length - 1; i >= 0; i--) {
|
|
127
|
+
const logger = this.loggers[i].deref();
|
|
128
|
+
if (logger) logger.level = effective;
|
|
129
|
+
else this.loggers.splice(i, 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
record(o: Record<string, unknown>, line: string): void {
|
|
134
|
+
const level = typeof o.level === 'number' ? o.level : LEVELS.info;
|
|
135
|
+
if (level < LEVELS[this.captureLevelValue]) return;
|
|
136
|
+
this.ring.push({
|
|
137
|
+
seq: ++this.seq,
|
|
138
|
+
time: typeof o.time === 'number' ? o.time : Date.now(),
|
|
139
|
+
level,
|
|
140
|
+
msg: typeof o.msg === 'string' ? o.msg : '',
|
|
141
|
+
line: line.length > LogTap.LINE_MAX ? `${line.slice(0, LogTap.LINE_MAX)}…` : line,
|
|
142
|
+
});
|
|
143
|
+
if (this.ring.length > LogTap.CAPACITY) this.ring.shift();
|
|
144
|
+
for (const cb of this.listeners) {
|
|
145
|
+
try {
|
|
146
|
+
cb();
|
|
147
|
+
} catch {
|
|
148
|
+
// A listener must never turn a log call into a throw.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Lines after `after` (0 = everything still buffered), oldest first. */
|
|
154
|
+
read(after = 0, limit = LogTap.CAPACITY): LogRead {
|
|
155
|
+
const first = this.ring[0]?.seq ?? this.seq + 1;
|
|
156
|
+
const fresh = this.ring.filter((e) => e.seq > after);
|
|
157
|
+
return {
|
|
158
|
+
entries: fresh.slice(Math.max(0, fresh.length - limit)),
|
|
159
|
+
head: this.seq,
|
|
160
|
+
dropped: Math.max(0, first - Math.max(after, this.clearedThrough) - 1),
|
|
161
|
+
consoleLevel: this.consoleLevel,
|
|
162
|
+
captureLevel: this.captureLevelValue,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
clear(): void {
|
|
167
|
+
this.ring.length = 0;
|
|
168
|
+
this.clearedThrough = this.seq;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Called synchronously inside the log call: keep it cheap, never log from it. */
|
|
172
|
+
subscribe(cb: () => void): () => void {
|
|
173
|
+
this.listeners.add(cb);
|
|
174
|
+
return () => void this.listeners.delete(cb);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createLogger(level: Level = 'info', transmit?: PinoTransmit, tap?: LogTap): Logger {
|
|
7
179
|
const browserConfig: LoggerOptions['browser'] = {
|
|
8
180
|
asObject: true,
|
|
9
181
|
write: (o: any) => {
|
|
10
|
-
|
|
182
|
+
const line = stringifyLog(o);
|
|
183
|
+
if (!tap || tap.printsToConsole(o?.level)) {
|
|
184
|
+
// oxlint-disable-next-line no-console
|
|
185
|
+
console.log(line);
|
|
186
|
+
}
|
|
187
|
+
tap?.record(o, line);
|
|
11
188
|
},
|
|
12
189
|
};
|
|
13
190
|
|
|
14
191
|
if (transmit) {
|
|
15
|
-
|
|
192
|
+
// Pinned to the configured level: a lower capture level for DevTools must
|
|
193
|
+
// not start shipping debug lines to the collector.
|
|
194
|
+
browserConfig.transmit = { ...transmit, level: transmit.level ?? level };
|
|
16
195
|
}
|
|
17
196
|
|
|
18
|
-
|
|
19
|
-
level,
|
|
197
|
+
const logger = pino({
|
|
198
|
+
level: tap ? tap.effectiveLevel : level,
|
|
20
199
|
browser: browserConfig,
|
|
200
|
+
onChild: (child) => tap?.track(child),
|
|
21
201
|
});
|
|
202
|
+
tap?.track(logger);
|
|
203
|
+
return logger;
|
|
22
204
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Logger } from 'pino';
|
|
2
|
-
import { PersistenceClient } from '../../types';
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
import type { PersistenceClient } from '../../types';
|
|
3
3
|
|
|
4
4
|
export class LocalStoragePersistenceClient implements PersistenceClient {
|
|
5
5
|
private logger: Logger;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Logger } from 'pino';
|
|
2
|
-
import { PersistenceClient } from '../../types';
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
import type { PersistenceClient } from '../../types';
|
|
3
3
|
|
|
4
4
|
export class ResilientPersistenceClient implements PersistenceClient {
|
|
5
5
|
private logger: Logger;
|
|
@@ -23,7 +23,14 @@ export class ResilientPersistenceClient implements PersistenceClient {
|
|
|
23
23
|
{ key, error: e, Category: 'sp00ky-client::ResilientPersistenceClient::get' },
|
|
24
24
|
'Persistence read failed, dropping key'
|
|
25
25
|
);
|
|
26
|
-
|
|
26
|
+
// Best-effort cleanup of the corrupt key; if removal also fails, the key
|
|
27
|
+
// just gets dropped again on the next failed read, so the failure is safe.
|
|
28
|
+
await this.inner.remove(key).catch((removeErr) => {
|
|
29
|
+
this.logger.debug(
|
|
30
|
+
{ key, error: removeErr, Category: 'sp00ky-client::ResilientPersistenceClient::get' },
|
|
31
|
+
'Failed to drop corrupt persistence key'
|
|
32
|
+
);
|
|
33
|
+
});
|
|
27
34
|
return null;
|
|
28
35
|
}
|
|
29
36
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { PersistenceClient } from '../../types';
|
|
1
|
+
import type { PersistenceClient } from '../../types';
|
|
2
2
|
import { parseRecordIdString, surql } from '../../utils/index';
|
|
3
|
-
import { Logger } from 'pino';
|
|
4
|
-
import {
|
|
3
|
+
import type { Logger } from 'pino';
|
|
4
|
+
import type { LocalStore } from '../database/cache-engine';
|
|
5
5
|
|
|
6
6
|
export class SurrealDBPersistenceClient implements PersistenceClient {
|
|
7
7
|
private logger: Logger;
|
|
8
8
|
|
|
9
9
|
constructor(
|
|
10
|
-
private db:
|
|
10
|
+
private db: LocalStore,
|
|
11
11
|
logger: Logger
|
|
12
12
|
) {
|
|
13
13
|
this.logger = logger.child({ service: 'PersistenceClient:SurrealDb' });
|