@parallax-rn/sdk 0.0.0-stage → 0.1.0-beta.2
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/LICENSE +72 -0
- package/README.md +31 -2
- package/dist/commands.d.ts +15 -0
- package/dist/commands.js +50 -0
- package/dist/console-capture.d.ts +7 -0
- package/dist/console-capture.js +56 -0
- package/dist/error-capture.d.ts +4 -0
- package/dist/error-capture.js +31 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.js +92 -0
- package/dist/host.d.ts +19 -0
- package/dist/host.js +8 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +32 -0
- package/dist/limits.d.ts +31 -0
- package/dist/limits.js +24 -0
- package/dist/navigation.d.ts +26 -0
- package/dist/navigation.js +153 -0
- package/dist/network-capture.d.ts +13 -0
- package/dist/network-capture.js +375 -0
- package/dist/network-redact.d.ts +13 -0
- package/dist/network-redact.js +108 -0
- package/dist/push.d.ts +36 -0
- package/dist/push.js +130 -0
- package/dist/ring-buffer.d.ts +26 -0
- package/dist/ring-buffer.js +46 -0
- package/dist/sdk.d.ts +35 -0
- package/dist/sdk.js +223 -0
- package/dist/snapshot.d.ts +9 -0
- package/dist/snapshot.js +151 -0
- package/dist/stores.d.ts +25 -0
- package/dist/stores.js +220 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +5 -0
- package/package.json +19 -4
package/dist/push.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ErrorEntry, type LogEntry, type SourceEntry } from '@parallax-rn/protocol';
|
|
2
|
+
import type { HostGlobal } from './host.js';
|
|
3
|
+
import type { SdkLimits } from './limits.js';
|
|
4
|
+
import type { RingBuffer } from './ring-buffer.js';
|
|
5
|
+
export interface PushDeps {
|
|
6
|
+
readonly host: HostGlobal;
|
|
7
|
+
readonly contextId: string;
|
|
8
|
+
readonly logs: RingBuffer<LogEntry>;
|
|
9
|
+
readonly errors: RingBuffer<ErrorEntry>;
|
|
10
|
+
/** The namespaced data sources (protocol 0.5) by name; rings may be added after the batcher exists. */
|
|
11
|
+
readonly sources: ReadonlyMap<string, RingBuffer<SourceEntry>>;
|
|
12
|
+
readonly limits: SdkLimits;
|
|
13
|
+
}
|
|
14
|
+
export declare class PushBatcher {
|
|
15
|
+
private readonly deps;
|
|
16
|
+
private timer;
|
|
17
|
+
private urgent;
|
|
18
|
+
private pushedLogs;
|
|
19
|
+
private pushedErrors;
|
|
20
|
+
private readonly pushedSources;
|
|
21
|
+
private disposed;
|
|
22
|
+
/** Pushes handed to the binding (for diagnostics and tests). */
|
|
23
|
+
pushes: number;
|
|
24
|
+
constructor(deps: PushDeps);
|
|
25
|
+
/** Called after an entry was appended. */
|
|
26
|
+
notify(urgent: boolean): void;
|
|
27
|
+
flush(): void;
|
|
28
|
+
/**
|
|
29
|
+
* An empty push that says this context's SDK has started. Nothing else makes a server that looked before the SDK started (the app was found without one, or the SDK was
|
|
30
|
+
* injected later) ask again until the first entry, and the producer of a source stays with the debugger until then.
|
|
31
|
+
*/
|
|
32
|
+
hello(): void;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
private send;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=push.d.ts.map
|
package/dist/push.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PushBatcher = void 0;
|
|
4
|
+
// Pushes new entries to the server through the CDP binding (decision 0010, spike 0006). Pushes only reduce latency: the ring
|
|
5
|
+
// buffers stay the source of truth and the server drains them by cursor, so a missed push loses nothing.
|
|
6
|
+
// - the payload is always a string (an object passed to the binding is silently dropped by Hermes);
|
|
7
|
+
// - the binding is checked with `typeof === 'function'` before every call (it does not exist in a fresh process until a
|
|
8
|
+
// session adds it);
|
|
9
|
+
// - bursts are coalesced: the first new entry starts a timer, everything that arrives within `flushIntervalMs` goes out
|
|
10
|
+
// together; errors are pushed at once;
|
|
11
|
+
// - one push is split when it would exceed the entry or size limits.
|
|
12
|
+
const protocol_1 = require("@parallax-rn/protocol");
|
|
13
|
+
const OVERHEAD = 80; // rough JSON overhead per entry, in characters
|
|
14
|
+
const weight = (entry) => OVERHEAD + entry.message.length + ('stack' in entry && typeof entry.stack === 'string' ? entry.stack.length : 0);
|
|
15
|
+
class PushBatcher {
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
this.timer = null;
|
|
19
|
+
this.urgent = false;
|
|
20
|
+
this.pushedLogs = 0;
|
|
21
|
+
this.pushedErrors = 0;
|
|
22
|
+
this.pushedSources = new Map();
|
|
23
|
+
this.disposed = false;
|
|
24
|
+
/** Pushes handed to the binding (for diagnostics and tests). */
|
|
25
|
+
this.pushes = 0;
|
|
26
|
+
this.pushedLogs = deps.logs.last;
|
|
27
|
+
this.pushedErrors = deps.errors.last;
|
|
28
|
+
}
|
|
29
|
+
/** Called after an entry was appended. */
|
|
30
|
+
notify(urgent) {
|
|
31
|
+
if (this.disposed)
|
|
32
|
+
return;
|
|
33
|
+
const { host, limits } = this.deps;
|
|
34
|
+
if (typeof host.setTimeout !== 'function') {
|
|
35
|
+
this.flush();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (this.timer !== null) {
|
|
39
|
+
if (!urgent || this.urgent)
|
|
40
|
+
return; // a timer is already pending, and it is not going to be sooner
|
|
41
|
+
if (typeof host.clearTimeout === 'function')
|
|
42
|
+
host.clearTimeout(this.timer);
|
|
43
|
+
}
|
|
44
|
+
this.urgent = urgent;
|
|
45
|
+
this.timer = host.setTimeout(() => this.flush(), urgent ? 0 : limits.flushIntervalMs);
|
|
46
|
+
}
|
|
47
|
+
flush() {
|
|
48
|
+
var _a;
|
|
49
|
+
const { host, logs, errors, sources, limits, contextId } = this.deps;
|
|
50
|
+
if (this.timer !== null && typeof host.clearTimeout === 'function')
|
|
51
|
+
host.clearTimeout(this.timer);
|
|
52
|
+
this.timer = null;
|
|
53
|
+
this.urgent = false;
|
|
54
|
+
const newLogs = logs.drain(this.pushedLogs, Infinity).entries;
|
|
55
|
+
const newErrors = errors.drain(this.pushedErrors, Infinity).entries;
|
|
56
|
+
// advance first: whether or not anyone listens, the buffers hold the history
|
|
57
|
+
this.pushedLogs = logs.last;
|
|
58
|
+
this.pushedErrors = errors.last;
|
|
59
|
+
const newSources = [];
|
|
60
|
+
for (const [name, ring] of sources) {
|
|
61
|
+
const drained = ring.drain((_a = this.pushedSources.get(name)) !== null && _a !== void 0 ? _a : 0, Infinity).entries;
|
|
62
|
+
this.pushedSources.set(name, ring.last);
|
|
63
|
+
if (drained.length > 0)
|
|
64
|
+
newSources.push([name, drained]);
|
|
65
|
+
}
|
|
66
|
+
if (newLogs.length === 0 && newErrors.length === 0 && newSources.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
let batch = { logs: [], errors: [], sources: {} };
|
|
69
|
+
let entries = 0;
|
|
70
|
+
let chars = 0;
|
|
71
|
+
const send = () => {
|
|
72
|
+
if (entries === 0)
|
|
73
|
+
return;
|
|
74
|
+
this.send(JSON.stringify({ contextId, logs: batch.logs, errors: batch.errors, ...(Object.keys(batch.sources).length > 0 ? { sources: batch.sources } : {}) }));
|
|
75
|
+
batch = { logs: [], errors: [], sources: {} };
|
|
76
|
+
entries = 0;
|
|
77
|
+
chars = 0;
|
|
78
|
+
};
|
|
79
|
+
const add = (entry, into) => {
|
|
80
|
+
const w = weight(entry);
|
|
81
|
+
if (entries > 0 && (entries >= limits.maxBatchEntries || chars + w > limits.maxBatchChars))
|
|
82
|
+
send();
|
|
83
|
+
batch[into].push(entry);
|
|
84
|
+
entries++;
|
|
85
|
+
chars += w;
|
|
86
|
+
};
|
|
87
|
+
const addSource = (name, entry) => {
|
|
88
|
+
var _a, _b;
|
|
89
|
+
var _c;
|
|
90
|
+
const w = OVERHEAD + ((_a = JSON.stringify(entry.data)) !== null && _a !== void 0 ? _a : '').length;
|
|
91
|
+
if (entries > 0 && (entries >= limits.maxBatchEntries || chars + w > limits.maxBatchChars))
|
|
92
|
+
send();
|
|
93
|
+
((_b = (_c = batch.sources)[name]) !== null && _b !== void 0 ? _b : (_c[name] = [])).push(entry);
|
|
94
|
+
entries++;
|
|
95
|
+
chars += w;
|
|
96
|
+
};
|
|
97
|
+
for (const entry of newErrors)
|
|
98
|
+
add(entry, 'errors');
|
|
99
|
+
for (const entry of newLogs)
|
|
100
|
+
add(entry, 'logs');
|
|
101
|
+
for (const [name, list] of newSources)
|
|
102
|
+
for (const entry of list)
|
|
103
|
+
addSource(name, entry);
|
|
104
|
+
send();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* An empty push that says this context's SDK has started. Nothing else makes a server that looked before the SDK started (the app was found without one, or the SDK was
|
|
108
|
+
* injected later) ask again until the first entry, and the producer of a source stays with the debugger until then.
|
|
109
|
+
*/
|
|
110
|
+
hello() {
|
|
111
|
+
this.send(JSON.stringify({ contextId: this.deps.contextId, logs: [], errors: [] }));
|
|
112
|
+
}
|
|
113
|
+
dispose() {
|
|
114
|
+
this.flush();
|
|
115
|
+
this.disposed = true;
|
|
116
|
+
}
|
|
117
|
+
send(payload) {
|
|
118
|
+
const binding = this.deps.host[protocol_1.PUSH_BINDING_NAME];
|
|
119
|
+
if (typeof binding !== 'function')
|
|
120
|
+
return;
|
|
121
|
+
this.pushes++;
|
|
122
|
+
try {
|
|
123
|
+
binding(payload);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* pushing must never break the app */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.PushBatcher = PushBatcher;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BufferState } from '@parallax-rn/protocol';
|
|
2
|
+
export interface Sequenced {
|
|
3
|
+
readonly seq: number;
|
|
4
|
+
}
|
|
5
|
+
export interface Drained<T> {
|
|
6
|
+
readonly entries: T[];
|
|
7
|
+
/** The highest seq returned, or the (clamped) cursor when nothing was returned. */
|
|
8
|
+
readonly next: number;
|
|
9
|
+
/** Entries after the cursor that were evicted before they could be returned. */
|
|
10
|
+
readonly dropped: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class RingBuffer<T extends Sequenced> {
|
|
13
|
+
readonly capacity: number;
|
|
14
|
+
private readonly slots;
|
|
15
|
+
private count;
|
|
16
|
+
private lastSeq;
|
|
17
|
+
private droppedTotal;
|
|
18
|
+
constructor(capacity: number);
|
|
19
|
+
/** Assigns the next seq, stores the entry and evicts the oldest one when full. */
|
|
20
|
+
append(make: (seq: number) => T): T;
|
|
21
|
+
get last(): number;
|
|
22
|
+
state(): BufferState;
|
|
23
|
+
/** Entries with seq greater than `cursor`, oldest first, at most `limit`. */
|
|
24
|
+
drain(cursor: number, limit: number): Drained<T>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=ring-buffer.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RingBuffer = void 0;
|
|
4
|
+
class RingBuffer {
|
|
5
|
+
constructor(capacity) {
|
|
6
|
+
this.capacity = capacity;
|
|
7
|
+
this.count = 0;
|
|
8
|
+
this.lastSeq = 0;
|
|
9
|
+
this.droppedTotal = 0;
|
|
10
|
+
if (!(capacity >= 1) || Math.floor(capacity) !== capacity)
|
|
11
|
+
throw new Error('capacity must be a positive integer');
|
|
12
|
+
this.slots = new Array(capacity);
|
|
13
|
+
}
|
|
14
|
+
/** Assigns the next seq, stores the entry and evicts the oldest one when full. */
|
|
15
|
+
append(make) {
|
|
16
|
+
const seq = ++this.lastSeq;
|
|
17
|
+
const entry = make(seq);
|
|
18
|
+
if (this.count === this.capacity)
|
|
19
|
+
this.droppedTotal++;
|
|
20
|
+
else
|
|
21
|
+
this.count++;
|
|
22
|
+
this.slots[(seq - 1) % this.capacity] = entry;
|
|
23
|
+
return entry;
|
|
24
|
+
}
|
|
25
|
+
get last() {
|
|
26
|
+
return this.lastSeq;
|
|
27
|
+
}
|
|
28
|
+
state() {
|
|
29
|
+
return { first: this.lastSeq - this.count + 1, last: this.lastSeq, dropped: this.droppedTotal };
|
|
30
|
+
}
|
|
31
|
+
/** Entries with seq greater than `cursor`, oldest first, at most `limit`. */
|
|
32
|
+
drain(cursor, limit) {
|
|
33
|
+
const first = this.lastSeq - this.count + 1;
|
|
34
|
+
const from = Math.max(cursor + 1, first);
|
|
35
|
+
const dropped = cursor + 1 < first ? first - (cursor + 1) : 0;
|
|
36
|
+
const entries = [];
|
|
37
|
+
for (let seq = from; seq <= this.lastSeq && entries.length < limit; seq++) {
|
|
38
|
+
const entry = this.slots[(seq - 1) % this.capacity];
|
|
39
|
+
if (entry)
|
|
40
|
+
entries.push(entry);
|
|
41
|
+
}
|
|
42
|
+
const lastReturned = entries.length > 0 ? entries[entries.length - 1] : undefined;
|
|
43
|
+
return { entries, next: lastReturned ? lastReturned.seq : Math.min(cursor, this.lastSeq), dropped };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.RingBuffer = RingBuffer;
|
package/dist/sdk.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Handshake, type SdkApi } from '@parallax-rn/protocol';
|
|
2
|
+
import { type CommandHandler, type CommandMeta } from './commands.js';
|
|
3
|
+
import { type NavigationRefLike } from './navigation.js';
|
|
4
|
+
import { type HostGlobal } from './host.js';
|
|
5
|
+
import { type SdkLimits } from './limits.js';
|
|
6
|
+
import { type StoreOptions } from './stores.js';
|
|
7
|
+
export interface SdkOptions {
|
|
8
|
+
readonly host?: HostGlobal;
|
|
9
|
+
readonly limits?: Partial<SdkLimits>;
|
|
10
|
+
readonly version?: string;
|
|
11
|
+
readonly now?: () => number;
|
|
12
|
+
/** Capture HTTP requests as the `network` source (default true). */
|
|
13
|
+
readonly network?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface Sdk {
|
|
16
|
+
/** Starts capturing. Returns false (and does nothing) when `__DEV__` is false. Idempotent. */
|
|
17
|
+
start(): boolean;
|
|
18
|
+
/** Stops capturing and restores the original console methods and error handler. */
|
|
19
|
+
stop(): void;
|
|
20
|
+
readonly started: boolean;
|
|
21
|
+
/** Changes whenever the JS context is recreated (the SDK module is evaluated again). */
|
|
22
|
+
readonly contextId: string;
|
|
23
|
+
readonly limits: SdkLimits;
|
|
24
|
+
readonly api: SdkApi;
|
|
25
|
+
registerCommand(name: string, handler: CommandHandler, meta?: CommandMeta): () => void;
|
|
26
|
+
/** Follows a navigation container ref (the `navigation` source); registering again replaces the previous one. Returns the stop function. */
|
|
27
|
+
registerNavigation(ref: NavigationRefLike): () => void;
|
|
28
|
+
/** Follows a store with `getState()` and `subscribe()` (Redux, Zustand) as the source `state:<name>`. Returns an unregister function. */
|
|
29
|
+
registerStore(name: string, store: unknown, options?: StoreOptions): () => void;
|
|
30
|
+
/** Follows a TanStack Query client as the source `query:<name>`. Returns an unregister function. */
|
|
31
|
+
registerQueryClient(name: string, client: unknown): () => void;
|
|
32
|
+
handshake(): Handshake;
|
|
33
|
+
}
|
|
34
|
+
export declare function createSdk(options?: SdkOptions): Sdk;
|
|
35
|
+
//# sourceMappingURL=sdk.d.ts.map
|
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSdk = createSdk;
|
|
4
|
+
// Assembles the SDK: ring buffers, console and error capture, push, the command registry and the API object under
|
|
5
|
+
// `globalThis.__PARALLAX_RN__.sdk` (decisions 0002, 0010, 0012). Nothing happens until `start()`.
|
|
6
|
+
const protocol_1 = require("@parallax-rn/protocol");
|
|
7
|
+
const commands_js_1 = require("./commands.js");
|
|
8
|
+
const console_capture_js_1 = require("./console-capture.js");
|
|
9
|
+
const error_capture_js_1 = require("./error-capture.js");
|
|
10
|
+
const navigation_js_1 = require("./navigation.js");
|
|
11
|
+
const network_capture_js_1 = require("./network-capture.js");
|
|
12
|
+
const format_js_1 = require("./format.js");
|
|
13
|
+
const host_js_1 = require("./host.js");
|
|
14
|
+
const limits_js_1 = require("./limits.js");
|
|
15
|
+
const push_js_1 = require("./push.js");
|
|
16
|
+
const ring_buffer_js_1 = require("./ring-buffer.js");
|
|
17
|
+
const stores_js_1 = require("./stores.js");
|
|
18
|
+
const version_js_1 = require("./version.js");
|
|
19
|
+
function newContextId(now) {
|
|
20
|
+
return `${now.toString(36)}-${Math.floor(Math.random() * 0x7fffffff).toString(36)}`;
|
|
21
|
+
}
|
|
22
|
+
function createSdk(options = {}) {
|
|
23
|
+
var _a, _b, _c;
|
|
24
|
+
const host = (_a = options.host) !== null && _a !== void 0 ? _a : (0, host_js_1.defaultHost)();
|
|
25
|
+
const now = (_b = options.now) !== null && _b !== void 0 ? _b : (() => Date.now());
|
|
26
|
+
const version = (_c = options.version) !== null && _c !== void 0 ? _c : version_js_1.SDK_VERSION;
|
|
27
|
+
const limits = { ...limits_js_1.DEFAULT_LIMITS, ...options.limits };
|
|
28
|
+
const startedAt = now();
|
|
29
|
+
const contextId = newContextId(startedAt);
|
|
30
|
+
const logs = new ring_buffer_js_1.RingBuffer(limits.logCapacity);
|
|
31
|
+
const errors = new ring_buffer_js_1.RingBuffer(limits.errorCapacity);
|
|
32
|
+
const registry = new commands_js_1.CommandRegistry();
|
|
33
|
+
// namespaced data sources (protocol 0.5) the SDK announces: a ring is added here only once its producer is installed
|
|
34
|
+
const sources = new Map();
|
|
35
|
+
const networkRing = new ring_buffer_js_1.RingBuffer(limits.networkCapacity);
|
|
36
|
+
const navigationRing = new ring_buffer_js_1.RingBuffer(limits.navigationCapacity);
|
|
37
|
+
let stopNavigation = null;
|
|
38
|
+
const batcher = new push_js_1.PushBatcher({ host, contextId, logs, errors, sources, limits });
|
|
39
|
+
// a store's ring is added on its first snapshot, which only happens while started (so a release build announces none)
|
|
40
|
+
const stores = new stores_js_1.StoreAdapters({
|
|
41
|
+
host,
|
|
42
|
+
limits,
|
|
43
|
+
emit: (name, data) => {
|
|
44
|
+
let ring = sources.get(name);
|
|
45
|
+
if (!ring)
|
|
46
|
+
sources.set(name, (ring = new ring_buffer_js_1.RingBuffer(limits.storeCapacity)));
|
|
47
|
+
ring.append(seq => ({ seq, time: now(), data }));
|
|
48
|
+
batcher.notify(false);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const captures = [];
|
|
52
|
+
let started = false;
|
|
53
|
+
registry.register('parallax.ping', () => ({ pong: true, time: now() }), { description: 'Checks that the SDK answers.' });
|
|
54
|
+
registry.register('parallax.info', () => handshake(), { description: 'The SDK handshake: versions, context id and buffer state.' });
|
|
55
|
+
function handshake() {
|
|
56
|
+
const base = { protocol: [protocol_1.PROTOCOL_VERSION[0], protocol_1.PROTOCOL_VERSION[1]], sdk: version, contextId, startedAt, buffers: { logs: logs.state(), errors: errors.state() } };
|
|
57
|
+
if (sources.size === 0)
|
|
58
|
+
return base;
|
|
59
|
+
const announced = {};
|
|
60
|
+
for (const [name, ring] of sources)
|
|
61
|
+
announced[name] = ring.state();
|
|
62
|
+
return { ...base, sources: announced };
|
|
63
|
+
}
|
|
64
|
+
function appendLog(level, args) {
|
|
65
|
+
const message = (0, format_js_1.cap)((0, format_js_1.formatArgs)(args), limits.maxMessageChars);
|
|
66
|
+
const time = now();
|
|
67
|
+
logs.append(seq => (message.truncated ? { seq, time, level, message: message.text, truncated: true } : { seq, time, level, message: message.text }));
|
|
68
|
+
batcher.notify(false);
|
|
69
|
+
const rejection = (0, protocol_1.unhandledRejectionOf)(level, args);
|
|
70
|
+
if (rejection)
|
|
71
|
+
appendError(rejection, false);
|
|
72
|
+
}
|
|
73
|
+
function appendError(error, fatal) {
|
|
74
|
+
var _a;
|
|
75
|
+
const source = typeof error === 'object' && error !== null ? error : null;
|
|
76
|
+
const message = (0, format_js_1.cap)(typeof (source === null || source === void 0 ? void 0 : source.message) === 'string' ? source.message : (0, format_js_1.formatArg)(error), limits.maxMessageChars);
|
|
77
|
+
const stack = typeof (source === null || source === void 0 ? void 0 : source.stack) === 'string' ? (0, format_js_1.cap)(source.stack, limits.maxStackChars) : null;
|
|
78
|
+
const name = typeof (source === null || source === void 0 ? void 0 : source.name) === 'string' ? source.name : null;
|
|
79
|
+
const time = now();
|
|
80
|
+
const truncated = message.truncated || ((_a = stack === null || stack === void 0 ? void 0 : stack.truncated) !== null && _a !== void 0 ? _a : false);
|
|
81
|
+
errors.append(seq => {
|
|
82
|
+
const entry = { seq, time, message: message.text, fatal };
|
|
83
|
+
if (name !== null)
|
|
84
|
+
entry.name = name;
|
|
85
|
+
if (stack !== null)
|
|
86
|
+
entry.stack = stack.text;
|
|
87
|
+
if (truncated)
|
|
88
|
+
entry.truncated = true;
|
|
89
|
+
return entry;
|
|
90
|
+
});
|
|
91
|
+
batcher.notify(true);
|
|
92
|
+
}
|
|
93
|
+
const api = {
|
|
94
|
+
handshake: () => JSON.stringify(handshake()),
|
|
95
|
+
drain(requestJson) {
|
|
96
|
+
var _a;
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(requestJson);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new Error(`invalid drain request: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
}
|
|
104
|
+
const request = (0, protocol_1.parseDrainRequest)(parsed);
|
|
105
|
+
if (!request.ok)
|
|
106
|
+
throw new Error(`invalid drain request: ${request.error}`);
|
|
107
|
+
const limit = Math.min((_a = request.value.limit) !== null && _a !== void 0 ? _a : limits.drainLimit, limits.drainLimit);
|
|
108
|
+
const result = { contextId, logs: logs.drain(request.value.logs, limit), errors: errors.drain(request.value.errors, limit) };
|
|
109
|
+
const asked = request.value.sources;
|
|
110
|
+
if (asked) {
|
|
111
|
+
result.sources = {};
|
|
112
|
+
for (const [name, ring] of sources)
|
|
113
|
+
if (Object.prototype.hasOwnProperty.call(asked, name))
|
|
114
|
+
result.sources[name] = ring.drain(asked[name], limit);
|
|
115
|
+
}
|
|
116
|
+
return JSON.stringify(result);
|
|
117
|
+
},
|
|
118
|
+
commands: () => JSON.stringify(registry.list()),
|
|
119
|
+
async call(requestJson) {
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
const request = (0, protocol_1.parseCommandCall)(JSON.parse(requestJson));
|
|
123
|
+
result = request.ok
|
|
124
|
+
? await registry.call(request.value.name, request.value.args === undefined ? null : request.value.args)
|
|
125
|
+
: { ok: false, error: { code: 'command_failed', message: `invalid command request: ${request.error}` } };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
result = { ok: false, error: { code: 'command_failed', message: `invalid command request: ${error instanceof Error ? error.message : String(error)}` } };
|
|
129
|
+
}
|
|
130
|
+
return JSON.stringify(result);
|
|
131
|
+
},
|
|
132
|
+
dispose: () => sdk.stop(),
|
|
133
|
+
};
|
|
134
|
+
function container() {
|
|
135
|
+
const existing = host[protocol_1.GLOBAL_NAME];
|
|
136
|
+
if (typeof existing === 'object' && existing !== null)
|
|
137
|
+
return existing;
|
|
138
|
+
const created = {};
|
|
139
|
+
host[protocol_1.GLOBAL_NAME] = created;
|
|
140
|
+
return created;
|
|
141
|
+
}
|
|
142
|
+
const sdk = {
|
|
143
|
+
get started() {
|
|
144
|
+
return started;
|
|
145
|
+
},
|
|
146
|
+
contextId,
|
|
147
|
+
limits,
|
|
148
|
+
api,
|
|
149
|
+
registerCommand: (name, handler, meta) => registry.register(name, handler, meta),
|
|
150
|
+
registerNavigation(ref) {
|
|
151
|
+
if (typeof ref !== 'object' || ref === null || typeof ref.getRootState !== 'function' || typeof ref.addListener !== 'function')
|
|
152
|
+
throw new TypeError('a navigation ref needs getRootState and addListener');
|
|
153
|
+
if (host.__DEV__ === false)
|
|
154
|
+
return () => undefined; // never active in a release build
|
|
155
|
+
if (stopNavigation)
|
|
156
|
+
stopNavigation(); // Fast Refresh re-runs modules: the new ref replaces the old one
|
|
157
|
+
sources.set('navigation', navigationRing);
|
|
158
|
+
const stop = (0, navigation_js_1.followNavigation)(ref, data => {
|
|
159
|
+
navigationRing.append(seq => ({ seq, time: now(), data }));
|
|
160
|
+
batcher.notify(false);
|
|
161
|
+
});
|
|
162
|
+
stopNavigation = stop;
|
|
163
|
+
return () => {
|
|
164
|
+
stop();
|
|
165
|
+
if (stopNavigation === stop)
|
|
166
|
+
stopNavigation = null;
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
registerStore: (name, store, options) => stores.registerStore(name, store, options),
|
|
170
|
+
registerQueryClient: (name, client) => stores.registerQueryClient(name, client),
|
|
171
|
+
handshake,
|
|
172
|
+
start() {
|
|
173
|
+
if (host.__DEV__ === false)
|
|
174
|
+
return false; // never active in a release build
|
|
175
|
+
if (started)
|
|
176
|
+
return true;
|
|
177
|
+
const target = container();
|
|
178
|
+
if (target.sdk && typeof target.sdk.dispose === 'function')
|
|
179
|
+
target.sdk.dispose(); // an older instance of this module (for example after a hot reload) steps aside
|
|
180
|
+
captures.push((0, console_capture_js_1.installConsoleCapture)(host, appendLog));
|
|
181
|
+
const errorCapture = (0, error_capture_js_1.installErrorCapture)(host, appendError);
|
|
182
|
+
if (errorCapture)
|
|
183
|
+
captures.push(errorCapture);
|
|
184
|
+
if (options.network !== false) {
|
|
185
|
+
const network = (0, network_capture_js_1.installNetworkCapture)(host, {
|
|
186
|
+
now,
|
|
187
|
+
bodyChars: limits.networkBodyChars,
|
|
188
|
+
headerChars: limits.networkHeaderChars,
|
|
189
|
+
stackChars: limits.networkStackChars,
|
|
190
|
+
onEntry: (data, time) => {
|
|
191
|
+
networkRing.append(seq => ({ seq, time, data }));
|
|
192
|
+
batcher.notify(false);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
if (network) {
|
|
196
|
+
captures.push(network);
|
|
197
|
+
sources.set('network', networkRing);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
target.sdk = api;
|
|
201
|
+
started = true;
|
|
202
|
+
stores.start();
|
|
203
|
+
batcher.hello();
|
|
204
|
+
return true;
|
|
205
|
+
},
|
|
206
|
+
stop() {
|
|
207
|
+
if (!started)
|
|
208
|
+
return;
|
|
209
|
+
started = false;
|
|
210
|
+
for (const capture of captures.splice(0))
|
|
211
|
+
capture.uninstall();
|
|
212
|
+
if (stopNavigation)
|
|
213
|
+
stopNavigation();
|
|
214
|
+
stopNavigation = null;
|
|
215
|
+
stores.stop();
|
|
216
|
+
batcher.flush();
|
|
217
|
+
const target = host[protocol_1.GLOBAL_NAME];
|
|
218
|
+
if (target && target.sdk === api)
|
|
219
|
+
delete target.sdk;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
return sdk;
|
|
223
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Snapshot {
|
|
2
|
+
readonly value: unknown;
|
|
3
|
+
/** True when anything was cut: depth, a container's keys, the character budget, a long string. */
|
|
4
|
+
readonly truncated: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function snapshotOf(value: unknown, maxChars: number, maxDepth: number): Snapshot;
|
|
7
|
+
/** Dotted paths (three levels deep) where two snapshots differ, at most 30. */
|
|
8
|
+
export declare function changedPaths(before: unknown, after: unknown): string[];
|
|
9
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.snapshotOf = snapshotOf;
|
|
4
|
+
exports.changedPaths = changedPaths;
|
|
5
|
+
// Turns an app's state into a JSON-safe, masked, size-bounded copy (plan 0013, 13.9). Nothing here calls app code: no getter runs, no toJSON, no function is invoked, so a hostile or
|
|
6
|
+
// huge state costs a bounded walk. Secret-looking keys are replaced and string values go through `scrubText` before the copy leaves the app.
|
|
7
|
+
const protocol_1 = require("@parallax-rn/protocol");
|
|
8
|
+
const MAX_KEYS = 100; // per object, array, map or set
|
|
9
|
+
const MAX_STRING = 500;
|
|
10
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
11
|
+
function put(out, key, value) {
|
|
12
|
+
if (key === '__proto__')
|
|
13
|
+
Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true });
|
|
14
|
+
else
|
|
15
|
+
out[key] = value;
|
|
16
|
+
}
|
|
17
|
+
function walkItems(items, depth, seen, budget) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (let i = 0; i < items.length; i++) {
|
|
20
|
+
if (budget.left <= 0 || i >= MAX_KEYS) {
|
|
21
|
+
budget.truncated = true;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
out.push(walk(items[i], depth, seen, budget));
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function walkObject(value, depth, seen, budget) {
|
|
29
|
+
const out = {};
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const key in value) {
|
|
32
|
+
if (!hasOwn(value, key))
|
|
33
|
+
continue;
|
|
34
|
+
if (budget.left <= 0 || count >= MAX_KEYS) {
|
|
35
|
+
budget.truncated = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
count++;
|
|
39
|
+
budget.left -= key.length + 4;
|
|
40
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
41
|
+
if (!descriptor || descriptor.get || descriptor.set)
|
|
42
|
+
put(out, key, '[Getter]');
|
|
43
|
+
else if ((0, protocol_1.isSecretKey)(key) && descriptor.value !== null && descriptor.value !== undefined)
|
|
44
|
+
put(out, key, protocol_1.REDACTED_TEXT);
|
|
45
|
+
else
|
|
46
|
+
put(out, key, walk(descriptor.value, depth, seen, budget));
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function walk(value, depth, seen, budget) {
|
|
51
|
+
switch (typeof value) {
|
|
52
|
+
case 'string': {
|
|
53
|
+
const text = value.length > MAX_STRING ? ((budget.truncated = true), value.slice(0, MAX_STRING) + '…') : value;
|
|
54
|
+
budget.left -= text.length + 2;
|
|
55
|
+
return (0, protocol_1.scrubText)(text);
|
|
56
|
+
}
|
|
57
|
+
case 'number':
|
|
58
|
+
budget.left -= 8;
|
|
59
|
+
return Number.isFinite(value) ? value : String(value);
|
|
60
|
+
case 'boolean':
|
|
61
|
+
budget.left -= 5;
|
|
62
|
+
return value;
|
|
63
|
+
case 'undefined':
|
|
64
|
+
budget.left -= 11;
|
|
65
|
+
return '[undefined]';
|
|
66
|
+
case 'bigint':
|
|
67
|
+
case 'symbol':
|
|
68
|
+
budget.left -= 12;
|
|
69
|
+
return String(value);
|
|
70
|
+
case 'function':
|
|
71
|
+
budget.left -= 20;
|
|
72
|
+
return value.name ? `[Function ${String(value.name)}]` : '[Function]';
|
|
73
|
+
default:
|
|
74
|
+
}
|
|
75
|
+
if (value === null) {
|
|
76
|
+
budget.left -= 4;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const object = value;
|
|
80
|
+
if (seen.indexOf(object) !== -1)
|
|
81
|
+
return '[Circular]';
|
|
82
|
+
if (object instanceof Date)
|
|
83
|
+
return Number.isNaN(object.getTime()) ? 'Invalid Date' : object.toISOString();
|
|
84
|
+
if (object instanceof Error)
|
|
85
|
+
return `[${object.name}: ${(0, protocol_1.scrubText)(String(object.message).slice(0, MAX_STRING))}]`;
|
|
86
|
+
const kind = Array.isArray(object) ? `Array(${object.length})` : object instanceof Map ? `Map(${object.size})` : object instanceof Set ? `Set(${object.size})` : 'Object';
|
|
87
|
+
if (depth >= budget.maxDepth) {
|
|
88
|
+
budget.truncated = true;
|
|
89
|
+
return `[${kind}]`;
|
|
90
|
+
}
|
|
91
|
+
seen.push(object);
|
|
92
|
+
let out;
|
|
93
|
+
if (Array.isArray(object)) {
|
|
94
|
+
if (object.length > MAX_KEYS)
|
|
95
|
+
budget.truncated = true;
|
|
96
|
+
out = walkItems(object.slice(0, MAX_KEYS), depth + 1, seen, budget);
|
|
97
|
+
}
|
|
98
|
+
else if (object instanceof Map || object instanceof Set) {
|
|
99
|
+
const items = [];
|
|
100
|
+
const iterator = object.entries();
|
|
101
|
+
for (let step = iterator.next(); !step.done; step = iterator.next()) {
|
|
102
|
+
if (items.length >= MAX_KEYS) {
|
|
103
|
+
budget.truncated = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
items.push(object instanceof Map ? step.value : step.value[0]);
|
|
107
|
+
}
|
|
108
|
+
out = { [`[${kind}]`]: walkItems(items, depth + 1, seen, budget) };
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
out = walkObject(object, depth + 1, seen, budget);
|
|
112
|
+
}
|
|
113
|
+
seen.pop();
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function snapshotOf(value, maxChars, maxDepth) {
|
|
117
|
+
const budget = { left: maxChars, maxDepth, truncated: false };
|
|
118
|
+
const copy = walk(value, 0, [], budget);
|
|
119
|
+
if (budget.left <= 0)
|
|
120
|
+
budget.truncated = true;
|
|
121
|
+
return { value: copy, truncated: budget.truncated };
|
|
122
|
+
}
|
|
123
|
+
const MAX_CHANGED = 30;
|
|
124
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
125
|
+
/** The path form of the desktop app and `app_data` (`user.items`, `a["x-y"]`): a key that is not a plain name is quoted. */
|
|
126
|
+
const pathOf = (parent, key) => (IDENT.test(key) ? (parent === '' ? key : `${parent}.${key}`) : `${parent}[${JSON.stringify(key)}]`);
|
|
127
|
+
const DIFF_DEPTH = 3;
|
|
128
|
+
function diff(before, after, path, depth, out) {
|
|
129
|
+
if (out.length >= MAX_CHANGED)
|
|
130
|
+
return;
|
|
131
|
+
const plain = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
132
|
+
if (plain(before) && plain(after) && depth < DIFF_DEPTH) {
|
|
133
|
+
const keys = Object.keys(before);
|
|
134
|
+
for (const key of Object.keys(after))
|
|
135
|
+
if (!hasOwn(before, key))
|
|
136
|
+
keys.push(key);
|
|
137
|
+
for (let i = 0; i < keys.length; i++) {
|
|
138
|
+
const key = keys[i];
|
|
139
|
+
diff(before[key], after[key], pathOf(path, key), depth + 1, out);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
143
|
+
out.push(path === '' ? '(root)' : path);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Dotted paths (three levels deep) where two snapshots differ, at most 30. */
|
|
147
|
+
function changedPaths(before, after) {
|
|
148
|
+
const out = [];
|
|
149
|
+
diff(before, after, '', 0, out);
|
|
150
|
+
return out;
|
|
151
|
+
}
|