flugrekorder 1.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rogier Spieker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # flugrekorder
2
+
3
+ [![npm version](https://img.shields.io/npm/v/flugrekorder)](https://www.npmjs.com/package/flugrekorder)
4
+ [![CI](https://github.com/rspieker/flugrekorder/actions/workflows/tests.yml/badge.svg)](https://github.com/rspieker/flugrekorder/actions/workflows/tests.yml)
5
+ [![license](https://img.shields.io/npm/l/flugrekorder)](LICENSE)
6
+
7
+ > A tireless, impartial, punctilious, incurious spectator. It witnesses every interaction. It takes note. It understands nothing. Remarkably, this is a feature.
8
+
9
+ Wraps any object, function, or array in a transparent `Proxy` and emits a structured `Rekording` for every Reflect trap that fires — get, set, apply, construct, and all others.
10
+
11
+ Design principle: **record structure, relay behaviour, understand nothing.**
12
+ The recorder has no knowledge of what it wraps. If a dependency adds new methods, they are recorded automatically.
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ npm install flugrekorder
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ ```ts
27
+ import { create, type Rekording } from 'flugrekorder';
28
+
29
+ const records: Rekording[] = [];
30
+ const p = create({ greet: (name: string) => `hello, ${name}` }, {
31
+ callback: (r) => records.push(r),
32
+ });
33
+
34
+ p.greet('world');
35
+
36
+ console.log(records.map((r) => `${r.id} ${r.trap}`));
37
+ // #1 get
38
+ // #2 apply
39
+ ```
40
+
41
+ ---
42
+
43
+ ## API
44
+
45
+ ### `create(target, options)`
46
+
47
+ Wraps `target` in a recording proxy and returns it. The proxy is transparent — all operations on it behave identically to the original.
48
+
49
+ ```ts
50
+ import { create, type Rekording } from 'flugrekorder';
51
+
52
+ const records: Rekording[] = [];
53
+ const p = create(target, { callback: (r) => records.push(r) });
54
+ ```
55
+
56
+ **`options`**
57
+
58
+ | Field | Type | Default | Description |
59
+ |---|---|---|---|
60
+ | `callback` | `(r: Rekording) => void` | — | Called synchronously with each record. Mutually exclusive with `stream`. |
61
+ | `stream` | `Writable` | — | Node.js Writable; records are written as newline-delimited JSON. Mutually exclusive with `callback`. |
62
+ | `id` | `number \| (() => string)` | `0` | Starting integer for the auto-incrementing ID sequence, or a custom generator. IDs take the form `#1`, `#2`, … unless overridden. |
63
+ | `recursive` | `boolean` | `true` | When `false`, only the root target is proxied. Values returned from traps are passed through as-is. |
64
+ | `only` | `string[]` | all traps | Allowlist of Reflect trap names to record. Traps not listed pass straight through to `Reflect` without emitting a record. |
65
+
66
+ One of `callback` or `stream` is required.
67
+
68
+ ---
69
+
70
+ ### `isFlugrekorder(value)`
71
+
72
+ Returns `true` if `value` is a proxy created by this module.
73
+
74
+ ```ts
75
+ import { create, isFlugrekorder } from 'flugrekorder';
76
+
77
+ const p = create({ nested: { x: 1 } }, { callback: () => {} });
78
+
79
+ isFlugrekorder(p); // true
80
+ isFlugrekorder(p.nested); // true — proxied recursively
81
+ isFlugrekorder({}); // false
82
+ isFlugrekorder(42); // false
83
+ ```
84
+
85
+ ---
86
+
87
+ ### `getOrigin(proxy)`
88
+
89
+ Returns the structured `Origin` of a proxy — how and from where it was created. Returns `null` for the root proxy and for non-proxies.
90
+
91
+ ```ts
92
+ import { create, getOrigin } from 'flugrekorder';
93
+
94
+ const p = create({ a: { v: 1 } }, { callback: () => {} });
95
+
96
+ getOrigin(p); // null (root)
97
+ getOrigin(p.a); // { trap: 'get', parent: '#1', key: 'a' }
98
+ ```
99
+
100
+ ---
101
+
102
+ ### `getAncestors(proxy)`
103
+
104
+ Walks the origin chain from the root proxy down to `proxy` and returns every step as an ordered array of `{ proxy, origin }` pairs, root first. Returns an empty array for non-proxies.
105
+
106
+ ```ts
107
+ import { create, getAncestors } from 'flugrekorder';
108
+
109
+ const p = create({ a: { b: { c: 1 } } }, { callback: () => {} });
110
+
111
+ getAncestors(p.a.b);
112
+ // [
113
+ // { proxy: <root>, origin: null },
114
+ // { proxy: <a>, origin: { trap: 'get', parent: '#1', key: 'a' } },
115
+ // { proxy: <b>, origin: { trap: 'get', parent: '#2', key: 'b' } },
116
+ // ]
117
+ ```
118
+
119
+ ---
120
+
121
+ ### `getPath(proxy)`
122
+
123
+ Produces a human-readable dotted path string. Function and constructor calls are annotated with `()`. Returns an empty string for the root proxy and for non-proxies.
124
+
125
+ ```ts
126
+ import { create, getPath } from 'flugrekorder';
127
+
128
+ const p = create({ a: { b: { fn: () => ({ v: 1 }) } } }, { callback: () => {} });
129
+
130
+ getPath(p); // ''
131
+ getPath(p.a); // 'a'
132
+ getPath(p.a.b.fn); // 'a.b.fn'
133
+ getPath(p.a.b.fn()); // 'a.b.fn()'
134
+ ```
135
+
136
+ ---
137
+
138
+ ### `getTarget(proxy)`
139
+
140
+ Returns the original unwrapped target of a proxy. Returns `null` for non-proxies.
141
+
142
+ ```ts
143
+ import { create, getTarget } from 'flugrekorder';
144
+
145
+ const target = { x: 1 };
146
+ const p = create(target, { callback: () => {} });
147
+
148
+ getTarget(p) === target; // true
149
+ getTarget({}); // null
150
+ ```
151
+
152
+ ---
153
+
154
+ ### `getProxyById(id, proxy)`
155
+
156
+ Looks up a proxy by its recorded ID within the same graph as `proxy`. Useful for resolving `{ $proxy: id }` references in recorded args and results back to live proxy objects. Returns `undefined` if the ID is not found.
157
+
158
+ ```ts
159
+ import { create, getProxyById, type Rekording } from 'flugrekorder';
160
+
161
+ const records: Rekording[] = [];
162
+ const p = create({ nested: { x: 1 } }, { callback: (r) => records.push(r) });
163
+
164
+ p.nested; // triggers a get, result is { $proxy: '#2' }
165
+
166
+ const id = (records[0].result as { $proxy: string }).$proxy; // '#2'
167
+ const nested = getProxyById(id, p); // returns the same proxy as p.nested
168
+ ```
169
+
170
+ ---
171
+
172
+ ## The `Rekording` shape
173
+
174
+ Every emitted record has this structure:
175
+
176
+ ```ts
177
+ type Rekording = {
178
+ id: string; // e.g. '#3'
179
+ trap: string; // Reflect trap name: 'get', 'set', 'apply', …
180
+ origin: {
181
+ trap: 'get' | 'set' | 'defineProperty' | 'getOwnPropertyDescriptor';
182
+ parent: string; // ID of the proxy this trap fired on
183
+ key: string; // property name (symbols are serialised to their string form)
184
+ } | {
185
+ trap: 'apply' | 'construct';
186
+ source: string; // ID of the function/constructor proxy that was called
187
+ } | null; // null for the root proxy
188
+ args: Serialized[]; // trap arguments
189
+ result: Serialized; // return value
190
+ };
191
+ ```
192
+
193
+ `Serialized` values are JSON-safe: primitives pass through unchanged, proxiable values become `{ $proxy: '<id>' }`, arrays are serialised element-by-element, plain objects are serialised by value (with circular-reference protection).
194
+
195
+ ---
196
+
197
+ ## Examples
198
+
199
+ ### Stream interactions to a file (NDJSON)
200
+
201
+ ```ts
202
+ import { createWriteStream } from 'node:fs';
203
+ import { create } from 'flugrekorder';
204
+
205
+ const log = createWriteStream('interactions.ndjson');
206
+ const tracked = create(myService, { stream: log });
207
+
208
+ // Every trap now writes one JSON line to interactions.ndjson
209
+ await tracked.processOrder(orderId);
210
+ ```
211
+
212
+ ### Record only method calls
213
+
214
+ ```ts
215
+ import { create, type Rekording } from 'flugrekorder';
216
+
217
+ const records: Rekording[] = [];
218
+ const p = create(myApi, {
219
+ callback: (r) => records.push(r),
220
+ only: ['apply'],
221
+ });
222
+
223
+ p.users.find({ active: true });
224
+
225
+ // records contains only 'apply' entries — one per function call
226
+ console.log(records.length); // 1
227
+ ```
228
+
229
+ ### Inspect the call graph after the fact
230
+
231
+ ```ts
232
+ import { create, getPath, getProxyById, type Rekording } from 'flugrekorder';
233
+
234
+ const records: Rekording[] = [];
235
+ const p = create(myService, { callback: (r) => records.push(r) });
236
+
237
+ myService.run(p);
238
+
239
+ // Find every method call and display its path
240
+ records
241
+ .filter((r) => r.trap === 'apply')
242
+ .forEach((r) => {
243
+ if (r.origin && 'source' in r.origin) {
244
+ const fn = getProxyById(r.origin.source, p);
245
+ if (fn) console.log('called:', getPath(fn));
246
+ }
247
+ });
248
+ ```
249
+
250
+ ### Custom ID sequence
251
+
252
+ ```ts
253
+ import { create } from 'flugrekorder';
254
+
255
+ // Prefix IDs with a session token for correlation across multiple recordings
256
+ const session = crypto.randomUUID();
257
+ let n = 0;
258
+ const p = create(target, {
259
+ callback: (r) => console.log(r.id),
260
+ id: () => `${session}:${++n}`,
261
+ });
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Types
267
+
268
+ ```ts
269
+ export type Proxiable = object | Function;
270
+
271
+ export type Serialized =
272
+ | string | number | boolean | bigint | null | undefined
273
+ | { readonly $proxy: string }
274
+ | Serialized[]
275
+ | { [key: string]: Serialized };
276
+
277
+ export type Origin =
278
+ | { trap: 'get' | 'set' | 'defineProperty' | 'getOwnPropertyDescriptor'; parent: string; key: string | symbol }
279
+ | { trap: 'apply' | 'construct'; source: string }
280
+ | null;
281
+
282
+ export type Rekording = {
283
+ id: string;
284
+ trap: string;
285
+ origin: { trap: string; parent?: string; key?: string; source?: string } | null;
286
+ args: Serialized[];
287
+ result: Serialized;
288
+ };
289
+ ```
@@ -0,0 +1,84 @@
1
+ import { Writable } from 'node:stream';
2
+
3
+ /** Any value that can be wrapped in a Proxy — objects and functions. */
4
+ type Proxiable = object | Function;
5
+ /** Reflect traps that operate on a property — carry a parent ID and key in their origin. */
6
+ type PropertyTrap = 'get' | 'set' | 'defineProperty' | 'getOwnPropertyDescriptor';
7
+ /** Reflect traps that invoke a callable — carry a source ID in their origin. */
8
+ type CallTrap = 'apply' | 'construct';
9
+ /** Describes how a proxy was created — which trap fired, on which parent, and under which key or source. Null for root proxies. */
10
+ type Origin = {
11
+ trap: PropertyTrap;
12
+ parent: string;
13
+ key: string | symbol;
14
+ } | {
15
+ trap: CallTrap;
16
+ source: string;
17
+ } | null;
18
+ /** Origin with symbol keys coerced to strings — safe to include in a Rekording. */
19
+ type SerializedOrigin = {
20
+ trap: PropertyTrap;
21
+ parent: string;
22
+ key: string;
23
+ } | {
24
+ trap: CallTrap;
25
+ source: string;
26
+ } | null;
27
+ /** A JSON-safe representation of any value. Proxiable values are replaced with `{ $proxy: id }` tags. */
28
+ type Serialized = string | number | boolean | bigint | null | undefined | {
29
+ readonly $proxy: string;
30
+ } | Array<Serialized> | {
31
+ [key: string]: Serialized;
32
+ };
33
+ /** A single recorded interaction — one Reflect trap firing on one proxy. */
34
+ type Rekording = {
35
+ id: string;
36
+ trap: string;
37
+ origin: SerializedOrigin;
38
+ args: Array<Serialized>;
39
+ result: Serialized;
40
+ };
41
+ type CreateOptions = {
42
+ id?: number | (() => string);
43
+ recursive?: boolean;
44
+ only?: string[];
45
+ } & ({
46
+ stream: Writable;
47
+ callback?: never;
48
+ } | {
49
+ callback: (record: Rekording) => void;
50
+ stream?: never;
51
+ });
52
+ /**
53
+ * Wraps `target` in a transparent recording proxy.
54
+ * Every Reflect trap that fires emits a `Rekording` to `callback` or `stream`.
55
+ */
56
+ declare function create<T extends Proxiable>(target: T, options: CreateOptions): T;
57
+ /** Returns `true` if `value` is a proxy created by this module. */
58
+ declare function isFlugrekorder(value: unknown): value is Proxiable;
59
+ /** Returns the original unwrapped target of a proxy, or `null` for non-proxies. */
60
+ declare function getTarget(pxy: Proxiable): Proxiable | null;
61
+ /**
62
+ * Looks up a proxy by its recorded ID within the same graph as `pxy`.
63
+ * Use this to resolve `{ $proxy: id }` references in recorded args and results back to live proxies.
64
+ */
65
+ declare function getProxyById(id: string, pxy: Proxiable): Proxiable | undefined;
66
+ /** Returns the `Origin` of a proxy — how and from where it was created. Returns `null` for root proxies and non-proxies. */
67
+ declare function getOrigin(pxy: Proxiable): Origin;
68
+ /**
69
+ * Walks the origin chain from the root proxy down to `pxy`.
70
+ * Returns an ordered array of `{ proxy, origin }` pairs, root first.
71
+ * Returns an empty array for non-proxies.
72
+ */
73
+ declare function getAncestors(pxy: Proxiable): Array<{
74
+ proxy: Proxiable;
75
+ origin: Origin;
76
+ }>;
77
+ /**
78
+ * Returns a human-readable dotted path string for a proxy.
79
+ * Function and constructor calls are annotated with `()`.
80
+ * Returns an empty string for the root proxy and for non-proxies.
81
+ */
82
+ declare function getPath(pxy: Proxiable): string;
83
+
84
+ export { type CallTrap, type Origin, type PropertyTrap, type Proxiable, type Rekording, type Serialized, create, getAncestors, getOrigin, getPath, getProxyById, getTarget, isFlugrekorder };
@@ -0,0 +1,84 @@
1
+ import { Writable } from 'node:stream';
2
+
3
+ /** Any value that can be wrapped in a Proxy — objects and functions. */
4
+ type Proxiable = object | Function;
5
+ /** Reflect traps that operate on a property — carry a parent ID and key in their origin. */
6
+ type PropertyTrap = 'get' | 'set' | 'defineProperty' | 'getOwnPropertyDescriptor';
7
+ /** Reflect traps that invoke a callable — carry a source ID in their origin. */
8
+ type CallTrap = 'apply' | 'construct';
9
+ /** Describes how a proxy was created — which trap fired, on which parent, and under which key or source. Null for root proxies. */
10
+ type Origin = {
11
+ trap: PropertyTrap;
12
+ parent: string;
13
+ key: string | symbol;
14
+ } | {
15
+ trap: CallTrap;
16
+ source: string;
17
+ } | null;
18
+ /** Origin with symbol keys coerced to strings — safe to include in a Rekording. */
19
+ type SerializedOrigin = {
20
+ trap: PropertyTrap;
21
+ parent: string;
22
+ key: string;
23
+ } | {
24
+ trap: CallTrap;
25
+ source: string;
26
+ } | null;
27
+ /** A JSON-safe representation of any value. Proxiable values are replaced with `{ $proxy: id }` tags. */
28
+ type Serialized = string | number | boolean | bigint | null | undefined | {
29
+ readonly $proxy: string;
30
+ } | Array<Serialized> | {
31
+ [key: string]: Serialized;
32
+ };
33
+ /** A single recorded interaction — one Reflect trap firing on one proxy. */
34
+ type Rekording = {
35
+ id: string;
36
+ trap: string;
37
+ origin: SerializedOrigin;
38
+ args: Array<Serialized>;
39
+ result: Serialized;
40
+ };
41
+ type CreateOptions = {
42
+ id?: number | (() => string);
43
+ recursive?: boolean;
44
+ only?: string[];
45
+ } & ({
46
+ stream: Writable;
47
+ callback?: never;
48
+ } | {
49
+ callback: (record: Rekording) => void;
50
+ stream?: never;
51
+ });
52
+ /**
53
+ * Wraps `target` in a transparent recording proxy.
54
+ * Every Reflect trap that fires emits a `Rekording` to `callback` or `stream`.
55
+ */
56
+ declare function create<T extends Proxiable>(target: T, options: CreateOptions): T;
57
+ /** Returns `true` if `value` is a proxy created by this module. */
58
+ declare function isFlugrekorder(value: unknown): value is Proxiable;
59
+ /** Returns the original unwrapped target of a proxy, or `null` for non-proxies. */
60
+ declare function getTarget(pxy: Proxiable): Proxiable | null;
61
+ /**
62
+ * Looks up a proxy by its recorded ID within the same graph as `pxy`.
63
+ * Use this to resolve `{ $proxy: id }` references in recorded args and results back to live proxies.
64
+ */
65
+ declare function getProxyById(id: string, pxy: Proxiable): Proxiable | undefined;
66
+ /** Returns the `Origin` of a proxy — how and from where it was created. Returns `null` for root proxies and non-proxies. */
67
+ declare function getOrigin(pxy: Proxiable): Origin;
68
+ /**
69
+ * Walks the origin chain from the root proxy down to `pxy`.
70
+ * Returns an ordered array of `{ proxy, origin }` pairs, root first.
71
+ * Returns an empty array for non-proxies.
72
+ */
73
+ declare function getAncestors(pxy: Proxiable): Array<{
74
+ proxy: Proxiable;
75
+ origin: Origin;
76
+ }>;
77
+ /**
78
+ * Returns a human-readable dotted path string for a proxy.
79
+ * Function and constructor calls are annotated with `()`.
80
+ * Returns an empty string for the root proxy and for non-proxies.
81
+ */
82
+ declare function getPath(pxy: Proxiable): string;
83
+
84
+ export { type CallTrap, type Origin, type PropertyTrap, type Proxiable, type Rekording, type Serialized, create, getAncestors, getOrigin, getPath, getProxyById, getTarget, isFlugrekorder };