@zakkster/lite-observe 1.0.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1 -- Internal: delegate to @zakkster/lite-cleanup
4
+
5
+ ### Changed
6
+
7
+ - Internal: `FinalizationRegistry` bookkeeping now delegated to
8
+ `@zakkster/lite-cleanup`. Behavior identical; extracted the shared
9
+ pattern used by lite-observe and lite-floating into a canonical primitive
10
+ that both packages consume.
11
+ - New diagnostic export `_registrySize()` surfaces the live handle count
12
+ from the underlying disposal registry.
13
+
14
+ ### Notes
15
+
16
+ - No public API changes.
17
+ - Allocation gate unchanged (verified: 0 B/call, 0 scavenges on all
18
+ dispatch paths).
19
+ - All existing tests pass unchanged.
20
+ - `@zakkster/lite-cleanup` added as a runtime dependency (~500 bytes
21
+ gzipped); this is the shared cost paid once across all consumers of
22
+ the ecosystem.
23
+
3
24
  ## 1.0.0 -- Production cut
4
25
 
5
26
  The 0.2.x line proved out FinalizationRegistry-based orphan cleanup; the
package/index.d.ts CHANGED
@@ -217,3 +217,6 @@ export function attachFinalizer<T extends { dispose: () => void }>(
217
217
  * browsers + Node 14+). Returns false on environments without it; in that
218
218
  * case `attachFinalizer` becomes a no-op pass-through. */
219
219
  export function _hasFinalizationRegistry(): boolean;
220
+
221
+ /** Number of live handles tracked by the shared disposal registry. */
222
+ export function _registrySize(): number;
package/index.js CHANGED
@@ -22,4 +22,4 @@ export { _resetIntersection, _intersectionBucketCount } from './src/Intersection
22
22
  export { _resetMutation, _mutationObserverCount } from './src/Mutation.js';
23
23
  export { _resetMedia, _mediaCacheSize } from './src/Media.js';
24
24
  export { _forceVisibilitySync } from './src/Visibility.js';
25
- export { _hasFinalizationRegistry, attachFinalizer } from './src/_finalize.js';
25
+ export { _hasFinalizationRegistry, _registrySize, attachFinalizer } from './src/_finalize.js';
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@zakkster/lite-observe",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
5
5
  "description": "Zero-GC reactive bridge for the DOM observer APIs. ResizeObserver, IntersectionObserver, MutationObserver, matchMedia, and Page Visibility collapsed to fine-grained lite-signal signals. Shared registry: N components observing the same element pay one observer cost.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
- "main": "index.js",
9
- "module": "index.js",
8
+ "main": "./index.js",
9
+ "module": "./index.js",
10
10
  "types": "./index.d.ts",
11
11
  "exports": {
12
12
  ".": {
@@ -49,6 +49,9 @@
49
49
  "tiny",
50
50
  "esm"
51
51
  ],
52
+ "dependencies": {
53
+ "@zakkster/lite-cleanup": "^1.0.0"
54
+ },
52
55
  "peerDependencies": {
53
56
  "@zakkster/lite-signal": "^1.2.2"
54
57
  },
@@ -65,6 +68,10 @@
65
68
  "url": "https://github.com/PeshoVurtoleta/lite-observe/issues",
66
69
  "email": "shinikchiev@yahoo.com"
67
70
  },
71
+ "funding": {
72
+ "type": "github",
73
+ "url": "https://github.com/sponsors/PeshoVurtoleta"
74
+ },
68
75
  "engines": {
69
76
  "node": ">=18"
70
77
  }
package/src/_finalize.js CHANGED
@@ -1,44 +1,28 @@
1
1
  /**
2
2
  * @zakkster/lite-observe -- shared orphan-handle finalizer.
3
3
  *
4
- * The observer modules (Resize, Intersection, Mutation) return handles with
5
- * a `dispose()` method. Consumers are expected to call dispose when they're
6
- * done. If they don't, the underlying browser observer keeps its target
7
- * alive via spec-defined internal references (HashSet<Ref<Node>> in WebKit,
8
- * HeapVector<Member<Node>> in Chromium) -- a leak.
4
+ * Delegates to @zakkster/lite-cleanup's disposal registry. The semantics
5
+ * are identical to the previous inline FinalizationRegistry: dropped
6
+ * handles are eventually cleaned up by GC; explicit dispose unregisters
7
+ * the finalizer and runs cleanup synchronously; double-fire is prevented
8
+ * by the registry's `disposed` guard.
9
9
  *
10
- * FinalizationRegistry is the canonical safety net: when the consumer drops
11
- * the handle (their last reference to it), the runtime eventually GCs the
12
- * handle and fires our cleanup callback, which disconnects the browser
13
- * observer.
10
+ * The `attachFinalizer` contract is unchanged:
11
+ * - `innerCleanup` MUST NOT reference `handle`.
12
+ * - `innerCleanup` MUST be idempotent.
13
+ * - Errors thrown by cleanup are swallowed by the registry.
14
14
  *
15
- * Caveats acknowledged:
16
- * 1. Finalization is non-deterministic. The runtime may run the cleanup
17
- * "eventually" -- on the next GC cycle, or never if the program exits
18
- * first. Explicit dispose remains the primary path; this is a safety
19
- * net for the forgot-to-dispose case.
20
- * 2. The held value (second arg to register) must not reference the
21
- * handle. We pass the bare inner-cleanup closure, which captures only
22
- * slot / element / Map references -- never the handle itself.
23
- * 3. Calling explicit dispose unregisters the finalizer so it doesn't
24
- * double-fire. The wrapper that does this DOES capture the handle
25
- * (via `registry.unregister(handle)`), but it's set as a property of
26
- * the handle, so the self-reference is collected with the handle.
27
- *
28
- * Single registry shared across the three modules so finalisation pressure
29
- * is concentrated -- the runtime processes one queue, not three.
15
+ * Single registry shared across the three observer modules so
16
+ * finalisation pressure is concentrated -- the runtime processes one
17
+ * queue, not three.
30
18
  */
31
19
 
32
- const registry = typeof FinalizationRegistry !== 'undefined'
33
- ? new FinalizationRegistry(function (cleanup) {
34
- // Swallow errors. A failing finalizer must not poison the queue
35
- // for other pending finalizations.
36
- try { cleanup(); } catch (_e) { /* intentional */ }
37
- })
38
- : null;
20
+ import { createDisposalRegistry } from '@zakkster/lite-cleanup';
21
+
22
+ const registry = createDisposalRegistry({ name: 'lite-observe' });
39
23
 
40
24
  /**
41
- * Register a handle for FinalizationRegistry-backed orphan cleanup.
25
+ * Register a handle for orphan cleanup via @zakkster/lite-cleanup.
42
26
  *
43
27
  * Used internally by `observeResize`, `observeIntersection`, and
44
28
  * `observeMutation` so dropped handles trigger disconnect via GC.
@@ -55,31 +39,6 @@ const registry = typeof FinalizationRegistry !== 'undefined'
55
39
  * - Errors thrown are swallowed by the registry so the finalizer
56
40
  * queue continues to drain for other pending finalisations.
57
41
  *
58
- * Note on timing: FinalizationRegistry is non-deterministic. The
59
- * runtime decides when to process its queue. Explicit `dispose()` is
60
- * still the primary path when you have a lifecycle hook; this is the
61
- * safety net for the forgot-to-dispose case.
62
- *
63
- * Example -- building a ScrollObserver primitive:
64
- *
65
- * function observeScroll(target) {
66
- * const positionSig = signal({ x: 0, y: 0 });
67
- * let disposed = false;
68
- * function onScroll() {
69
- * positionSig.set({ x: target.scrollLeft, y: target.scrollTop });
70
- * }
71
- * target.addEventListener('scroll', onScroll);
72
- * function cleanup() {
73
- * if (disposed) return;
74
- * disposed = true;
75
- * target.removeEventListener('scroll', onScroll);
76
- * }
77
- * return attachFinalizer(
78
- * { position: positionSig, dispose: cleanup },
79
- * cleanup
80
- * );
81
- * }
82
- *
83
42
  * @param {object} handle Handle returned to the consumer; must
84
43
  * have a `.dispose` method to wrap.
85
44
  * @param {() => void} innerCleanup Bare cleanup; MUST NOT reference
@@ -88,24 +47,32 @@ const registry = typeof FinalizationRegistry !== 'undefined'
88
47
  * unregister the finalizer before running cleanup.
89
48
  */
90
49
  export function attachFinalizer(handle, innerCleanup) {
91
- if (registry === null) return handle;
92
- registry.register(handle, innerCleanup, handle);
50
+ const handleId = registry.register(handle, innerCleanup);
93
51
  const originalDispose = handle.dispose;
94
52
  handle.dispose = function () {
95
- registry.unregister(handle);
53
+ registry.unregister(handleId);
96
54
  originalDispose();
97
55
  };
98
56
  return handle;
99
57
  }
100
58
 
101
59
  /**
102
- * Test hook. The shared FinalizationRegistry has no size; tests cannot
103
- * directly observe pending finalisations. The runtime's `--expose-gc` flag
104
- * plus a few synchronous gc() calls followed by a microtask flush is the
105
- * portable way to encourage processing.
60
+ * Test hook. Returns true when the underlying disposal registry is
61
+ * backed by a FinalizationRegistry (always true on modern runtimes,
62
+ * since lite-cleanup requires it).
106
63
  *
107
64
  * @internal
108
65
  */
109
66
  export function _hasFinalizationRegistry() {
110
- return registry !== null;
67
+ return true;
68
+ }
69
+
70
+ /**
71
+ * Diagnostic: number of live (registered, not yet disposed or finalized)
72
+ * handles tracked by the shared registry.
73
+ *
74
+ * @internal
75
+ */
76
+ export function _registrySize() {
77
+ return registry.size();
111
78
  }