@actualwave/deferred-data-access 2.0.0 → 2.1.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.
Files changed (128) hide show
  1. package/README.md +400 -27
  2. package/SKILL.md +106 -0
  3. package/command/index.d.ts.map +1 -0
  4. package/command/index.es.js +98 -0
  5. package/command/index.es.js.map +1 -0
  6. package/command/index.js +102 -4
  7. package/command/index.js.map +1 -1
  8. package/command/package.json +4 -1
  9. package/command/src/command-chain.d.ts +8 -1
  10. package/command/src/command-chain.d.ts.map +1 -0
  11. package/command/src/command-chain.js +9 -0
  12. package/command/src/command-chain.js.map +1 -1
  13. package/command/src/command-handler.d.ts.map +1 -0
  14. package/command/src/command.d.ts +2 -2
  15. package/command/src/command.d.ts.map +1 -0
  16. package/command/src/command.js +5 -6
  17. package/command/src/command.js.map +1 -1
  18. package/core/core.d.ts.map +1 -0
  19. package/core/core.js +23 -25
  20. package/core/core.js.map +1 -1
  21. package/core/index.d.ts.map +1 -0
  22. package/deferred-data-access.umd.js +2 -0
  23. package/deferred-data-access.umd.js.map +1 -0
  24. package/index.d.ts.map +1 -0
  25. package/{dist/deferred-data-access.js → index.es.js} +199 -138
  26. package/index.es.js.map +1 -0
  27. package/index.js +627 -2
  28. package/index.js.map +1 -1
  29. package/interface/index.d.ts.map +1 -0
  30. package/interface/index.es.js +380 -0
  31. package/interface/index.es.js.map +1 -0
  32. package/interface/index.js +396 -7
  33. package/interface/index.js.map +1 -1
  34. package/interface/package.json +4 -1
  35. package/interface/src/handshake.d.ts.map +1 -0
  36. package/interface/src/handshake.js +6 -4
  37. package/interface/src/handshake.js.map +1 -1
  38. package/interface/src/helpers.d.ts.map +1 -0
  39. package/interface/src/intialize.d.ts +5 -2
  40. package/interface/src/intialize.d.ts.map +1 -0
  41. package/interface/src/intialize.js +37 -57
  42. package/interface/src/intialize.js.map +1 -1
  43. package/interface/src/request.d.ts +3 -2
  44. package/interface/src/request.d.ts.map +1 -0
  45. package/interface/src/request.js +39 -29
  46. package/interface/src/request.js.map +1 -1
  47. package/interface/src/types.d.ts.map +1 -0
  48. package/interface/src/utils.d.ts +2 -2
  49. package/interface/src/utils.d.ts.map +1 -0
  50. package/interface/src/utils.js +44 -29
  51. package/interface/src/utils.js.map +1 -1
  52. package/package.json +7 -6
  53. package/proxy/index.d.ts.map +1 -0
  54. package/proxy/index.es.js +144 -0
  55. package/proxy/index.es.js.map +1 -0
  56. package/proxy/index.js +155 -5
  57. package/proxy/index.js.map +1 -1
  58. package/proxy/package.json +4 -1
  59. package/proxy/src/command.d.ts.map +1 -0
  60. package/proxy/src/proxy.d.ts +2 -2
  61. package/proxy/src/proxy.d.ts.map +1 -0
  62. package/proxy/src/proxy.js +13 -3
  63. package/proxy/src/proxy.js.map +1 -1
  64. package/proxy/src/traps.d.ts +1 -1
  65. package/proxy/src/traps.d.ts.map +1 -0
  66. package/proxy/src/traps.js +4 -14
  67. package/proxy/src/traps.js.map +1 -1
  68. package/proxy/src/types.d.ts.map +1 -0
  69. package/proxy/src/utils.d.ts +6 -0
  70. package/proxy/src/utils.d.ts.map +1 -0
  71. package/proxy/src/utils.js +11 -5
  72. package/proxy/src/utils.js.map +1 -1
  73. package/record/index.d.ts.map +1 -0
  74. package/record/index.es.js +26 -0
  75. package/record/index.es.js.map +1 -0
  76. package/record/index.js +31 -2
  77. package/record/index.js.map +1 -1
  78. package/record/package.json +4 -1
  79. package/record/record.d.ts +2 -2
  80. package/record/record.d.ts.map +1 -0
  81. package/record/record.js +9 -3
  82. package/record/record.js.map +1 -1
  83. package/resource/index.d.ts +1 -0
  84. package/resource/index.d.ts.map +1 -0
  85. package/resource/index.es.js +191 -0
  86. package/resource/index.es.js.map +1 -0
  87. package/resource/index.js +206 -6
  88. package/resource/index.js.map +1 -1
  89. package/resource/package.json +4 -1
  90. package/resource/src/default-resource-pool.d.ts +1 -0
  91. package/resource/src/default-resource-pool.d.ts.map +1 -0
  92. package/resource/src/default-resource-pool.js +8 -5
  93. package/resource/src/default-resource-pool.js.map +1 -1
  94. package/resource/src/finalization-registry.d.ts +13 -0
  95. package/resource/src/finalization-registry.d.ts.map +1 -0
  96. package/resource/src/finalization-registry.js +18 -0
  97. package/resource/src/finalization-registry.js.map +1 -0
  98. package/resource/src/resource-pool-registry.d.ts +1 -0
  99. package/resource/src/resource-pool-registry.d.ts.map +1 -0
  100. package/resource/src/resource-pool-registry.js +10 -8
  101. package/resource/src/resource-pool-registry.js.map +1 -1
  102. package/resource/src/resource-pool.d.ts +8 -1
  103. package/resource/src/resource-pool.d.ts.map +1 -0
  104. package/resource/src/resource-pool.js +29 -17
  105. package/resource/src/resource-pool.js.map +1 -1
  106. package/resource/src/resource.d.ts +1 -1
  107. package/resource/src/resource.d.ts.map +1 -0
  108. package/resource/src/resource.js +3 -2
  109. package/resource/src/resource.js.map +1 -1
  110. package/resource/src/utils.d.ts +1 -1
  111. package/resource/src/utils.d.ts.map +1 -0
  112. package/resource/src/utils.js +9 -1
  113. package/resource/src/utils.js.map +1 -1
  114. package/utils/index.d.ts.map +1 -0
  115. package/utils/index.es.js +48 -0
  116. package/utils/index.es.js.map +1 -0
  117. package/utils/index.js +54 -3
  118. package/utils/index.js.map +1 -1
  119. package/utils/package.json +4 -1
  120. package/utils/src/types.d.ts +3 -3
  121. package/utils/src/types.d.ts.map +1 -0
  122. package/utils/src/utils.d.ts +18 -2
  123. package/utils/src/utils.d.ts.map +1 -0
  124. package/utils/src/utils.js +28 -4
  125. package/utils/src/utils.js.map +1 -1
  126. package/dist/deferred-data-access.js.map +0 -1
  127. package/dist/deferred-data-access.umd.js +0 -2
  128. package/dist/deferred-data-access.umd.js.map +0 -1
package/README.md CHANGED
@@ -1,39 +1,412 @@
1
1
  # Deferred Data Access
2
- Deferred Data Access(DDA) is a framework for building async CRUD APIs. It transforms object calls into commands that can be interpreted by custom function.
2
+
3
+ Deferred Data Access (DDA) is a TypeScript library that wraps any object or Promise in a `Proxy` and records every property access, assignment, deletion, and function call as a typed **command**. A developer-supplied handler function decides what each command means — fetching from a REST API, forwarding to a Web Worker, replaying in a test, or anything else.
4
+
5
+ ## Table of contents
6
+
7
+ - [Installation](#installation)
8
+ - [Quick start](#quick-start)
9
+ - [How it works](#how-it-works)
10
+ - [Lazy vs reactive mode](#lazy-vs-reactive-mode)
11
+ - [Command types](#command-types)
12
+ - [Command handler](#command-handler)
13
+ - [createCommandHandler](#createcommandhandler)
14
+ - [CommandChain API](#commandchain-api)
15
+ - [Resource system](#resource-system)
16
+ - [Cross-context interface](#cross-context-interface)
17
+ - [Sub-package reference](#sub-package-reference)
18
+ - [Projects built on DDA](#projects-built-on-dda)
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @actualwave/deferred-data-access
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick start
31
+
32
+ ```typescript
33
+ import { handle } from '@actualwave/deferred-data-access';
34
+ import { ProxyCommand } from '@actualwave/deferred-data-access/proxy';
35
+
36
+ // 1. Define a handler that interprets commands
37
+ const handler = async (command, context, wrap) => {
38
+ if (command.type === ProxyCommand.GET) {
39
+ const target = await context;
40
+ return target[command.name];
41
+ }
42
+ // ...
43
+ };
44
+
45
+ // 2. Create a wrap factory for a given handler
46
+ const wrap = handle(handler);
47
+
48
+ // 3. Wrap an object (or a Promise of one) — returns a Proxy
49
+ const proxy = wrap({ user: { name: 'Alice' } });
50
+
51
+ // 4. Access properties — in lazy mode handler is called once on .then()
52
+ const name = await proxy.user.name; // → 'Alice'
53
+ ```
54
+
55
+ ---
3
56
 
4
57
  ## How it works
5
- 1. Framework accepts an async handler function and returns an object.
6
- ```javascript
7
- const obj = handle((command, context) => {
8
- let result;
9
58
 
10
- // Some logic that gets somewhere value and saves in into "result" variable.
59
+ 1. `handle(handler)` returns a `wrap` factory.
60
+ 2. `wrap(target)` wraps `target` in a `Proxy`. Every property access, assignment, deletion, or call is intercepted.
61
+ 3. Intercepted operations are chained into a `CommandChain` — a linked list of `ICommand` nodes, head to tail.
62
+ 4. When the result is awaited (`.then()` / `await`), the handler receives the full chain plus a context Promise and must return a Promise with the resolved value.
63
+ 5. The handler can call `wrap(result, command)` to return a new proxy that continues the same chain, enabling deeply nested lazy access.
64
+
65
+ ---
66
+
67
+ ## Lazy vs reactive mode
68
+
69
+ `handle(handler, lazy?)` — second argument controls the mode (default: `true`).
70
+
71
+ ### Lazy mode (default)
72
+
73
+ The handler is called **once** per `.then()` / `.catch()` invocation. Intermediate GET/APPLY operations build up a `CommandChain` but do not invoke the handler until the result is awaited.
74
+
75
+ ```typescript
76
+ const wrap = handle(handler); // lazy = true
77
+
78
+ const proxy = wrap(rootObject);
79
+ proxy.a.b.c; // no handler calls yet
80
+ await proxy.a.b.c; // handler called once with chain: GET(c) → GET(b) → GET(a)
81
+ ```
82
+
83
+ The command delivered to the handler is the **head** of the chain. Walk `command.prev` to reach earlier operations.
84
+
85
+ ### Reactive mode
86
+
87
+ The handler is called on **every** intercepted operation.
88
+
89
+ ```typescript
90
+ const wrap = handle(handler, false); // lazy = false
91
+
92
+ const proxy = wrap(rootObject);
93
+ proxy.a; // handler called: GET('a')
94
+ proxy.a.b; // handler called: GET('a'), then GET('b') on its result
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Command types
100
+
101
+ Imported from `@actualwave/deferred-data-access/proxy`:
102
+
103
+ | Constant | Value | Triggered by |
104
+ |---|---|---|
105
+ | `ProxyCommand.GET` | `'P:get'` | `proxy.prop` |
106
+ | `ProxyCommand.SET` | `'P:set'` | `proxy.prop = value` |
107
+ | `ProxyCommand.DELETE_PROPERTY` | `'P:del'` | `delete proxy.prop` |
108
+ | `ProxyCommand.APPLY` | `'P:apply'` | `proxy(args)` |
109
+ | `ProxyCommand.METHOD_CALL` | `'P:call'` | `proxy.method(args)` *(lazy mode only — collapses GET + APPLY)* |
11
110
 
12
- return result;
13
- })();
111
+ `METHOD_CALL` is only generated in lazy mode when a `GET` is immediately followed by an `APPLY`. It carries the method name in `command.name` and the arguments in `command.value`.
112
+
113
+ ### Command shape
114
+
115
+ ```typescript
116
+ interface ICommand {
117
+ type: string; // ProxyCommand value
118
+ name?: PropertyName; // string | symbol — property name
119
+ value?: unknown; // SET value, APPLY args, or METHOD_CALL args
120
+ context?: Promise<unknown>; // Promise resolving to the target object
121
+ }
14
122
  ```
15
- 2. Developer accesses object property and gets Promise
16
- ```javascript
17
- const value = await obj.prop;
123
+
124
+ ---
125
+
126
+ ## Command handler
127
+
128
+ ```typescript
129
+ type CommandHandler = (
130
+ command: ICommandList,
131
+ context: CommandContext | undefined,
132
+ wrap: (context: CommandContext, command?: ICommandChain) => unknown
133
+ ) => Promise<unknown>;
134
+ ```
135
+
136
+ | Argument | Description |
137
+ |---|---|
138
+ | `command` | Head of the `CommandChain` — inspect `command.type`, `command.name`, `command.value`. Walk `command.prev` for earlier operations. |
139
+ | `context` | A `Promise` resolving to the target object the command was issued against. `undefined` for the root call. |
140
+ | `wrap` | Re-wraps a new Promise with the same handler, enabling chained lazy access on sub-objects. |
141
+
142
+ ### Example — object property access
143
+
144
+ ```typescript
145
+ const handler = async (command, context, wrap) => {
146
+ const target = await context;
147
+
148
+ switch (command.type) {
149
+ case ProxyCommand.GET:
150
+ return target[command.name];
151
+
152
+ case ProxyCommand.SET:
153
+ target[command.name] = command.value;
154
+ return;
155
+
156
+ case ProxyCommand.METHOD_CALL:
157
+ return target[command.name](...command.value);
158
+
159
+ case ProxyCommand.APPLY:
160
+ return (target as Function)(...command.value);
161
+
162
+ case ProxyCommand.DELETE_PROPERTY:
163
+ return delete target[command.name];
164
+ }
165
+ };
18
166
  ```
19
- 3. Framework records property access into a command object, handler function is called with command object as an argument.
20
- ```javascript
21
- { type: ProxyCommand.GET, name: 'prop' }
167
+
168
+ ---
169
+
170
+ ## createCommandHandler
171
+
172
+ A utility that dispatches to per-type handler functions:
173
+
174
+ ```typescript
175
+ import { createCommandHandler } from '@actualwave/deferred-data-access/command';
176
+ import { ProxyCommand } from '@actualwave/deferred-data-access/proxy';
177
+
178
+ const handler = createCommandHandler({
179
+ handlers: {
180
+ [ProxyCommand.GET]: async (command, context) => {
181
+ const target = await context;
182
+ return target[command.name];
183
+ },
184
+ [ProxyCommand.SET]: async (command, context) => {
185
+ const target = await context;
186
+ target[command.name] = command.value;
187
+ },
188
+ },
189
+ defaultHandler: async (command) => {
190
+ throw new Error(`Unhandled command: ${command.type}`);
191
+ },
192
+ });
193
+
194
+ const wrap = handle(handler, false);
22
195
  ```
23
- 4. Whatever function returns is returned as promise value.
24
196
 
25
- DDA can operate in two modes, mode is defined by passing second argument to handle() function.
26
- * lazy - Default mode, custom handler function is called only when `then()` or `catch()` methods are accessed. Accessing `obj.child.grand.prop.then(func)` calls function once.
27
- * reactive - Non-lazy, custom handler function called on every operation. Accessing `obj.child.grand.prop.then(func)` calls function 3 times.
197
+ If no matching handler and no `defaultHandler`, the call resolves to `undefined`.
198
+
199
+ ---
200
+
201
+ ## CommandChain API
202
+
203
+ `CommandChain` extends `Command` and is iterable from head to tail:
204
+
205
+ ```typescript
206
+ import { CommandChain } from '@actualwave/deferred-data-access/command';
207
+
208
+ // Iterate head → tail
209
+ for (const node of command) {
210
+ console.log(node.type, node.name);
211
+ }
212
+
213
+ // Functional traversal
214
+ const types = command.map(node => node.type);
215
+ const path = command.reduce((acc, node) => [node.name, ...acc], []);
216
+
217
+ command.forEach(node => { /* head → tail */ });
218
+ command.isTail(); // true if this node has no prev
219
+ ```
220
+
221
+ ### withoutPrev()
222
+
223
+ Creates an immutable copy of the node with the `prev` link removed. Use this instead of mutating `prev` directly to avoid corrupting shared chain references:
224
+
225
+ ```typescript
226
+ const severed = command.withoutPrev();
227
+ // severed.prev === undefined
228
+ // command.prev is unchanged
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Resource system
234
+
235
+ Resources allow objects to be referenced by ID across serialisation boundaries (e.g. `postMessage`). Each `Resource` gets a stable string ID and lives in a `ResourcePool`.
236
+
237
+ ```typescript
238
+ import {
239
+ ResourcePool,
240
+ getDefaultResourcePool,
241
+ isResourceObject,
242
+ } from '@actualwave/deferred-data-access/resource';
243
+
244
+ const pool = getDefaultResourcePool(); // lazily-created singleton
245
+
246
+ // Register an object
247
+ const resource = pool.set(myObject);
248
+ console.log(resource.id); // stable string ID
249
+ console.log(resource.poolId); // pool's own ID
250
+
251
+ // Serialise for postMessage
252
+ const descriptor = resource.toObject();
253
+ // { id: '...', poolId: '...', type: 'object' }
254
+
255
+ // Reconstruct on the other side
256
+ const retrieved = pool.getById(descriptor.id);
257
+
258
+ // Type guard
259
+ if (isResourceObject(value)) {
260
+ const live = pool.getById(value.id);
261
+ }
262
+ ```
263
+
264
+ Multiple pools can be managed through `ResourcePoolRegistry`:
265
+
266
+ ```typescript
267
+ import { getRegistry } from '@actualwave/deferred-data-access/resource';
268
+
269
+ const registry = getRegistry(); // lazily-created singleton
270
+ const pool = registry.createPool();
271
+ registry.get(pool.id); // → pool
272
+ ```
273
+
274
+ ### Custom FinalizationRegistry
275
+
276
+ In environments where `globalThis.FinalizationRegistry` is absent or needs to be replaced (e.g. React Native / Hermes), set a custom implementation before any pools are created:
277
+
278
+ ```typescript
279
+ import { setCustomFinalizationRegistryClass } from '@actualwave/deferred-data-access/resource';
280
+
281
+ setCustomFinalizationRegistryClass(MyFinalizationRegistryPolyfill);
282
+ // All ResourcePools created after this call will use MyFinalizationRegistryPolyfill.
283
+ // Pass null to explicitly disable GC-based cleanup.
284
+ ```
285
+
286
+ The constructor of `ResourcePool` also accepts a `FinalizationRegistry` directly, which takes precedence over the module-level setting:
287
+
288
+ ```typescript
289
+ const pool = new ResourcePool(MyFinalizationRegistryPolyfill);
290
+ ```
291
+
292
+ ### Replacing singletons
293
+
294
+ ```typescript
295
+ import {
296
+ setDefaultResourcePool,
297
+ setRegistry,
298
+ } from '@actualwave/deferred-data-access/resource';
299
+
300
+ setDefaultResourcePool(myPool); // replace the default pool singleton
301
+ setRegistry(myRegistry); // replace the default registry singleton
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Cross-context interface
307
+
308
+ `initialize()` sets up a **bidirectional proxy channel** between two contexts (main thread ↔ Worker, two iframes, WebSocket peers, etc.). Each side runs `initialize()` and they perform a handshake before exposing proxies to each other's `root` object.
309
+
310
+ ```typescript
311
+ import { initialize, InterfaceType } from '@actualwave/deferred-data-access/interface';
312
+
313
+ // --- Main thread (HOST) ---
314
+ const worker = new Worker('./worker.js');
315
+
316
+ const { root, stop } = await initialize({
317
+ type: InterfaceType.HOST,
318
+ root: { greet: (name: string) => `Hello, ${name}!` }, // expose to worker
319
+ subscribe: (fn) => worker.addEventListener('message', fn),
320
+ unsubscribe: (fn) => worker.removeEventListener('message', fn),
321
+ sendMessage: (data) => worker.postMessage(data),
322
+ handshakeTimeout: 5000,
323
+ responseTimeout: 10000,
324
+ });
325
+
326
+ // `root` is a proxy to the worker's exported API
327
+ const result = await root.remoteMethod('arg');
328
+ stop(); // detach message listener
329
+ ```
330
+
331
+ ```typescript
332
+ // --- Worker (GUEST) ---
333
+ import { initialize, InterfaceType } from '@actualwave/deferred-data-access/interface';
334
+
335
+ await initialize({
336
+ type: InterfaceType.GUEST,
337
+ root: { remoteMethod: (arg: string) => arg.toUpperCase() },
338
+ subscribe: (fn) => self.addEventListener('message', fn),
339
+ unsubscribe: (fn) => self.removeEventListener('message', fn),
340
+ sendMessage: (data) => self.postMessage(data),
341
+ handshakeTimeout: 5000,
342
+ responseTimeout: 10000,
343
+ });
344
+ ```
345
+
346
+ ### InitConfig options
347
+
348
+ | Option | Type | Default | Description |
349
+ |---|---|---|---|
350
+ | `type` | `InterfaceType` | required | `HOST` waits for the guest; `GUEST` initiates |
351
+ | `root` | `unknown` | — | Object to expose to the remote side |
352
+ | `subscribe` | `(fn) => void` | required | Attach a message listener |
353
+ | `unsubscribe` | `(fn) => void` | required | Detach a message listener |
354
+ | `sendMessage` | `(data) => void` | required | Send a message to the remote side |
355
+ | `id` | `string` | auto | Stable ID for this interface endpoint |
356
+ | `remoteId` | `string` | — | Expected remote ID (skips handshake if both sides know each other's ID) |
357
+ | `handshakeTimeout` | `number` | — | ms before handshake times out |
358
+ | `handshakeInterval` | `number` | — | ms between handshake retry attempts (GUEST only) |
359
+ | `responseTimeout` | `number` | `0` (none) | ms before a remote call times out |
360
+ | `preprocessResponse` | `(data) => unknown` | identity | Transform raw message data before parsing |
361
+
362
+ ### initialize() return value
363
+
364
+ ```typescript
365
+ {
366
+ stop: () => void; // detach the message listener
367
+ pool: ResourcePool; // local resource pool
368
+ root: unknown | null; // proxy to the remote root (null if remote has no root)
369
+ wrap: Function; // wrap factory with the same handler (for advanced use)
370
+ pendingRequests: Map<…>; // in-flight request map (for advanced use)
371
+ }
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Sub-package reference
377
+
378
+ | Import path | Key exports |
379
+ |---|---|
380
+ | `@actualwave/deferred-data-access` | `handle` |
381
+ | `@actualwave/deferred-data-access/command` | `Command`, `CommandChain`, `createCommandHandler` |
382
+ | `@actualwave/deferred-data-access/proxy` | `ProxyCommand`, `wrapWithProxy`, `isWrappedWithProxy`, `unwrapProxy`, `generateProxyCommand` |
383
+ | `@actualwave/deferred-data-access/resource` | `Resource`, `ResourcePool`, `ResourcePoolRegistry`, `getDefaultResourcePool`, `setDefaultResourcePool`, `getRegistry`, `setRegistry`, `getCustomFinalizationRegistryClass`, `setCustomFinalizationRegistryClass`, `isResourceObject`, `createResource` |
384
+ | `@actualwave/deferred-data-access/record` | `recordHandlerCalls`, `latestCall`, `latestCallFor`, `clearLatestCalls` |
385
+ | `@actualwave/deferred-data-access/utils` | `IdOwner`, `generateId`, `createUIDGenerator`, `isReservedPropertyName`, `ReservedPropertyNames`, `reject` |
386
+ | `@actualwave/deferred-data-access/interface` | `initialize`, `InterfaceType`, `MessageType`, `createSubscriberFns`, `findEventEmitter`, `findMessagePort` |
387
+
388
+ ### recordHandlerCalls
389
+
390
+ Wraps a `CommandHandler` to track the latest in-flight call Promise, useful for implementing loading indicators or sequential request logic:
391
+
392
+ ```typescript
393
+ import { recordHandlerCalls, latestCall, latestCallFor } from '@actualwave/deferred-data-access/record';
394
+
395
+ const trackedHandler = recordHandlerCalls(myHandler);
396
+ const wrap = handle(trackedHandler);
397
+
398
+ const proxy = wrap(rootObject);
399
+ proxy.doWork();
400
+
401
+ // latestCall() returns the Promise of the most recent handler invocation
402
+ await latestCall();
403
+
404
+ // latestCallFor(context) returns the Promise for a specific context Promise
405
+ ```
28
406
 
29
- There are multiple predefined commands that can be passed to handler function.
30
- * ProxyCommand.**GET** - when property accessed
31
- * ProxyCommand.**SET** - when property is set, new value is recorded as `value` in command object
32
- * ProxyCommand.**DELETE_PROPERTY** - when property is being deleted
33
- * ProxyCommand.**APPLY** - when function is called, call arguments recorded as `value` of command object
34
- * ProxyCommand.**METHOD_CALL** - only generated in lazy mode, it combines GET and APPLY commands.
407
+ ---
35
408
 
409
+ ## Projects built on DDA
36
410
 
37
- ## Projects made with DDA
38
- * [RESTObject](https://github.com/burdiuz/js-deferred-data-access/tree/master/packages/rest-object)
39
- * [WorkerInterface](https://github.com/burdiuz/js-deferred-data-access/tree/master/packages/worker-interface)
411
+ - [RESTObject](https://github.com/burdiuz/js-deferred-data-access/tree/master/packages/rest-object) REST API access using dot notation
412
+ - [WorkerInterface](https://github.com/burdiuz/js-deferred-data-access/tree/master/packages/worker-interface) — transparent proxy to a Web Worker's API
package/SKILL.md ADDED
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: "deferred-data-access"
3
+ description: "TypeScript library that wraps objects in an ES Proxy and converts every property access, assignment, deletion, and call into a typed command delivered to a developer-supplied async handler. Use when working on or extending this package; building handlers that depend on it (REST object, worker proxy, etc.); reading or writing command chains; setting up cross-context communication via initialize(); working with ResourcePool and Resource; or writing and debugging tests for DDA-based code."
4
+ license: "MIT"
5
+ compatibility: "Node.js 18+. TypeScript 5+. Tests use Jest 30 + ts-jest. Run `npm test` from packages/deferred-data-access."
6
+ metadata:
7
+ author: "Oleg Galaburda <burdiuz@gmail.com>"
8
+ version: "2.0.0"
9
+ package: "@actualwave/deferred-data-access"
10
+ ---
11
+
12
+ # Deferred Data Access — Agent Skill
13
+
14
+ ## Sub-packages
15
+
16
+ | Import path | Key exports |
17
+ |---|---|
18
+ | `@actualwave/deferred-data-access` | `handle` |
19
+ | `.../command` | `Command`, `CommandChain`, `createCommandHandler` |
20
+ | `.../proxy` | `ProxyCommand`, `wrapWithProxy`, `isWrappedWithProxy`, `unwrapProxy`, `generateProxyCommand` |
21
+ | `.../resource` | `Resource`, `ResourcePool`, `ResourcePoolRegistry`, `getDefaultResourcePool`, `setDefaultResourcePool`, `getRegistry`, `setRegistry`, `getCustomFinalizationRegistryClass`, `setCustomFinalizationRegistryClass`, `isResourceObject`, `createResource` |
22
+ | `.../record` | `recordHandlerCalls`, `latestCall`, `latestCallFor`, `clearLatestCalls` |
23
+ | `.../utils` | `IdOwner`, `generateId`, `createUIDGenerator`, `isReservedPropertyName`, `ReservedPropertyNames`, `reject` |
24
+ | `.../interface` | `initialize`, `InterfaceType`, `MessageType`, `createSubscriberFns`, `findEventEmitter`, `findMessagePort` |
25
+
26
+ ## Core workflow
27
+
28
+ ```
29
+ handle(handler, lazy?) → wrap(target) → Proxy
30
+ → property access / call / set
31
+ → CommandChain (head → prev → … → tail)
32
+ → .then() / await → handler(command, context, wrap)
33
+ → Promise<result>
34
+ ```
35
+
36
+ ## Command types (`ProxyCommand`)
37
+
38
+ | Constant | Triggered by |
39
+ |---|---|
40
+ | `GET` `'P:get'` | `proxy.prop` |
41
+ | `SET` `'P:set'` | `proxy.prop = v` |
42
+ | `DELETE_PROPERTY` `'P:del'` | `delete proxy.prop` |
43
+ | `APPLY` `'P:apply'` | `proxy(args)` |
44
+ | `METHOD_CALL` `'P:call'` | `proxy.method(args)` *(lazy only)* |
45
+
46
+ ## Lazy vs reactive
47
+
48
+ - **Lazy** (default): handler fires once on `await`. Consecutive GET + APPLY collapses to `METHOD_CALL`. Walk `command.prev` for the full chain.
49
+ - **Reactive** (`handle(h, false)`): fires on every operation. `METHOD_CALL` never emitted; `APPLY` has no `name` — derive it from the preceding GET's path segment.
50
+
51
+ ## CommandChain
52
+
53
+ ```typescript
54
+ command.forEach(n => path.push(String(n.name))); // deepest-first
55
+ const nodes = [...command]; // head … tail via Symbol.iterator
56
+ command.withoutPrev(); // immutable copy, prev severed — never mutate prev directly
57
+ ```
58
+
59
+ ## Resource system
60
+
61
+ All singletons are lazily created — no pool or registry is instantiated at module load time.
62
+
63
+ ```typescript
64
+ import {
65
+ getDefaultResourcePool, setDefaultResourcePool,
66
+ getRegistry, setRegistry,
67
+ getCustomFinalizationRegistryClass, setCustomFinalizationRegistryClass,
68
+ } from '@actualwave/deferred-data-access/resource';
69
+ ```
70
+
71
+ - `getDefaultResourcePool()` / `setDefaultResourcePool(pool)` — module-level default `ResourcePool` singleton.
72
+ - `getRegistry()` / `setRegistry(registry)` — module-level `ResourcePoolRegistry` singleton. The registry auto-registers the default pool on first creation.
73
+ - `getCustomFinalizationRegistryClass()` / `setCustomFinalizationRegistryClass(cls)` — module-level fallback `FinalizationRegistry` class passed to every new `ResourcePool`. Set this before any pool is created in environments where `globalThis.FinalizationRegistry` is absent (e.g. Hermes/React Native). Pass `null` to explicitly disable GC cleanup.
74
+ - `ResourcePool` constructor: `new ResourcePool(FinalizationRegistry?)` — explicit argument takes precedence over the module-level setting.
75
+ - `ResourcePoolRegistry.createPool()` — creates and registers a new pool; uses the module-level `FinalizationRegistry` class via `ResourcePool`'s constructor default.
76
+
77
+ ## `initialize()` (cross-context)
78
+
79
+ ```typescript
80
+ import { initialize, InterfaceType } from '@actualwave/deferred-data-access/interface';
81
+ const { root, stop } = await initialize({
82
+ type: InterfaceType.HOST, // or GUEST
83
+ root: myApi,
84
+ subscribe: fn => emitter.on('message', fn),
85
+ unsubscribe: fn => emitter.off('message', fn),
86
+ sendMessage: data => transport.send(data),
87
+ preprocessResponse: e => e.data,
88
+ handshakeTimeout: 5_000,
89
+ });
90
+ ```
91
+
92
+ ## Testing
93
+
94
+ ```bash
95
+ cd packages/deferred-data-access && npm test
96
+ ```
97
+
98
+ Jest 30, ts-jest. `moduleNameMapper` in `jest.config.js` + `paths` in `tsconfig.spec.json`.
99
+
100
+ ## Key invariants
101
+
102
+ - `typeof null === 'object'` — always guard `value != null && typeof value === 'object'`.
103
+ - DDA always wraps root context in `Promise.resolve()`. Avoid unconditional `await`; check `isPromiseLike` first.
104
+ - `APPLY` in reactive mode has no `name` — derive from preceding GET's path.
105
+ - `Command.type`, `Command.name`, `Resource.pool`, `IdOwner.id` are runtime non-writable.
106
+ - `isReservedPropertyName('then')` → `true`; symbol keys always → `false`.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../packages/deferred-data-access/command/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,98 @@
1
+ class Command {
2
+ constructor(type, name, value, context) {
3
+ this.type = type;
4
+ this.name = name;
5
+ this.value = value;
6
+ this.context = context;
7
+ Object.defineProperty(this, 'type', { value: type, writable: false, configurable: false });
8
+ Object.defineProperty(this, 'name', { value: name, writable: false, configurable: false });
9
+ }
10
+ toObject(includeContext = false) {
11
+ const { type, name, value, context } = this;
12
+ return {
13
+ type,
14
+ name,
15
+ value,
16
+ context: includeContext ? context : undefined,
17
+ };
18
+ }
19
+ toJSON(includeContext = false) {
20
+ const { type, name, value, context } = this;
21
+ return includeContext
22
+ ? JSON.stringify([type, name, value, context])
23
+ : JSON.stringify([type, name, value]);
24
+ }
25
+ static fromJSON(jsonString) {
26
+ const [type, name, value, context] = JSON.parse(jsonString);
27
+ return new Command(type, name, value, context);
28
+ }
29
+ }
30
+
31
+ class CommandChain extends Command {
32
+ constructor(prev, type, name, value, context) {
33
+ super(type, name, value, context);
34
+ this.prev = prev;
35
+ }
36
+ *[Symbol.iterator]() {
37
+ let item = this;
38
+ while (item) {
39
+ yield item;
40
+ item = item.prev;
41
+ }
42
+ }
43
+ isTail() {
44
+ return !this.prev;
45
+ }
46
+ forEach(callback) {
47
+ let node = this;
48
+ do {
49
+ callback(node);
50
+ node = node.prev;
51
+ } while (node);
52
+ }
53
+ map(callback) {
54
+ let node = this;
55
+ const list = [];
56
+ do {
57
+ list.push(callback(node));
58
+ node = node.prev;
59
+ } while (node);
60
+ return list;
61
+ }
62
+ reduce(callback, base) {
63
+ let node = this;
64
+ let result = base;
65
+ do {
66
+ result = callback(result, node);
67
+ node = node.prev;
68
+ } while (node);
69
+ return result;
70
+ }
71
+ /**
72
+ * Returns a new CommandChain that is identical to this one but with the
73
+ * `prev` link severed, rather than mutating the existing instance in-place.
74
+ * Use this instead of `delete command.prev` to avoid corrupting shared chain
75
+ * references held by other code.
76
+ */
77
+ withoutPrev() {
78
+ return new CommandChain(undefined, this.type, this.name, this.value, this.context);
79
+ }
80
+ static fromCommand({ type, name, value, context }, prev) {
81
+ return new CommandChain(prev, type, name, value, context);
82
+ }
83
+ }
84
+
85
+ /*
86
+ Creates a function that calls handler depending on command type
87
+ */
88
+ const createCommandHandler = ({ handlers, defaultHandler, }) => (command, ...args) => {
89
+ const { type } = command;
90
+ const handler = (handlers && handlers[type]) || defaultHandler;
91
+ if (handler) {
92
+ return handler(command, ...args);
93
+ }
94
+ return Promise.resolve(undefined);
95
+ };
96
+
97
+ export { Command, CommandChain, createCommandHandler };
98
+ //# sourceMappingURL=index.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.es.js","sources":["../../../../../../packages/deferred-data-access/command/src/command.ts","../../../../../../packages/deferred-data-access/command/src/command-chain.ts","../../../../../../packages/deferred-data-access/command/src/command-handler.ts"],"sourcesContent":[null,null,null],"names":[],"mappings":"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"}