@cplieger/reactive 1.2.4 → 1.2.5
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/README.md +6 -5
- package/jsr.json +1 -1
- package/package.json +8 -5
- package/src/bind-list.ts +16 -3
- package/src/collection.ts +11 -3
- package/src/reconcile-tree.ts +33 -9
- package/src/signal.ts +21 -2
- package/src/store.ts +14 -2
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@cplieger/reactive)
|
|
4
4
|
[](https://jsr.io/@cplieger/reactive)
|
|
5
5
|
[](https://github.com/cplieger/reactive/actions/workflows/coverage.yml)
|
|
6
|
+
[](https://github.com/cplieger/reactive/issues?q=label%3Astryker-tracker)
|
|
6
7
|
[](https://www.bestpractices.dev/projects/13218)
|
|
7
8
|
[](https://scorecard.dev/viewer/?uri=github.com/cplieger/reactive)
|
|
8
9
|
|
|
@@ -75,7 +76,7 @@ isComputed(doubled); // true
|
|
|
75
76
|
- `flushSync()` — synchronously drain pending effects (no-op inside batch)
|
|
76
77
|
- `untracked<T>(fn): T` — run fn without tracking signal reads (like Preact `untracked()` / Solid `untrack()`)
|
|
77
78
|
- `on(deps, fn, options?): () => U` — explicit dependency declaration helper (like Solid `on()`). Returns `() => U`; with `{ defer: true }` the accessor returns `() => U | undefined` (the deferred first call yields `undefined`). A single accessor `() => T` types `value`/`prev` as `T`; an array of accessors types them as `unknown[]`. Pass into `effect()` or `computed()`.
|
|
78
|
-
- `subscribe(signal, cb): () => void` — subscribe to a signal, calling cb immediately and on every change
|
|
79
|
+
- `subscribe(signal, cb): () => void` — subscribe to a signal, calling cb immediately and on every change. The callback runs untracked: signals read inside it do not become dependencies of the subscription
|
|
79
80
|
- `isSignal(value): boolean` — type guard for signals created by `signal()`
|
|
80
81
|
- `isComputed(value): boolean` — type guard for computed signals
|
|
81
82
|
- `setEffectErrorHandler(handler): EffectErrorHandler` — set global error handler for effects; returns the previous handler
|
|
@@ -84,7 +85,7 @@ isComputed(doubled); // true
|
|
|
84
85
|
|
|
85
86
|
- `el(tag, attrs?, ...children)` — CSP-safe element factory (the build half; `reconcile`/`patch` are the commit half). `className` → class; `on*` → handler property + `trackHandler` (so `patch` reconciles handlers); boolean DOM props (`hidden`/`disabled`/`checked`/`selected`/…) and `value`/`colSpan`/`rowSpan`/`tabIndex`/`htmlFor` → properties; everything else (`data-*`, `aria-*`, `style`, `id`) → `setAttribute`. String children become text nodes (never parsed as HTML); null/undefined attrs and children are skipped.
|
|
86
87
|
- `reconcile<T>(parent, items, spec)` — keyed-list DOM reconciliation with mount/update/onRemove lifecycle
|
|
87
|
-
- `patch(parent, ...children)` — structural tree-diff, replacing a parent's children with reconciled new nodes. Element nodes are keyed by
|
|
88
|
+
- `patch(parent, ...children)` — structural tree-diff, replacing a parent's children with reconciled new nodes. Element nodes are keyed by a `*-id` attribute (specific entity id — takes precedence) or, failing that, `data-col` (so reorders/re-patches reuse the matched node); duplicate-key siblings match in document order; unkeyed nodes match by position.
|
|
88
89
|
- `reconcileChildren(parent, newChildren)` — low-level child reconciliation against existing DOM
|
|
89
90
|
- `trackHandler(el, key)` — register an `on*` property for handler reconciliation during tree-diff
|
|
90
91
|
|
|
@@ -92,7 +93,7 @@ isComputed(doubled); // true
|
|
|
92
93
|
|
|
93
94
|
`createStore` and `SignalMap` are thin facades over the one signal engine — there is no second reactivity implementation. `createStore` lazily backs each key with a signal; `SignalMap` is a registry of signals keyed by a runtime string id.
|
|
94
95
|
|
|
95
|
-
- `createStore<M>(): Store<M>` — typed, fixed-key reactive store with `get`, `set`, `subscribe`, `effect`, `computed`, and `batch`. `subscribe` notifies on change only (not immediately on subscribe). A `computed` key whose fn reads its own output does not loop unbounded: a self-read that keeps yielding new values trips the engine's batch-iteration guard and surfaces `Error("Cycle detected")` through the effect error handler (effects isolate errors, so it is not rethrown to the caller), while a stable self-read settles via `Object.is` dedup.
|
|
96
|
+
- `createStore<M>(): Store<M>` — typed, fixed-key reactive store with `get`, `set`, `subscribe`, `effect`, `computed`, and `batch`. `subscribe` notifies on change only (not immediately on subscribe), and its callback runs untracked. `computed(outputKey, fn)` is an EAGER effect that writes `outputKey` on every dependency change — not a lazy engine computed — so `fn` runs whether or not anyone reads the key, a throwing `fn` routes to the effect error handler instead of being cached and rethrown at the read site, and `set(outputKey, …)` still works between recomputes. A `computed` key whose fn reads its own output does not loop unbounded: a self-read that keeps yielding new values trips the engine's batch-iteration guard and surfaces `Error("Cycle detected")` through the effect error handler (effects isolate errors, so it is not rethrown to the caller), while a stable self-read settles via `Object.is` dedup.
|
|
96
97
|
- `SignalMap<V>` — dynamic per-id signal registry: `get(id)`, `ensure(id, initial)`, `clear(id)`, `clearAll()`. For reactive state whose key set isn't known at the type level (per-message streaming text, per-row state, …); complements `createStore`'s fixed key set.
|
|
97
98
|
|
|
98
99
|
### Collections
|
|
@@ -100,7 +101,7 @@ isComputed(doubled); // true
|
|
|
100
101
|
A reactive ordered collection of keyed entities — the data half of the two-tier list pattern. A per-entity content update touches only that entity's subscribers; add/remove/reorder bumps the structure signal (`ids`). Built on `signal` + `SignalMap`.
|
|
101
102
|
|
|
102
103
|
- `createCollection<T>(keyOf: (item: T) => string): Collection<T>` — returns a collection with:
|
|
103
|
-
- `setAll(items)` — replace everything (same-order replacement does not bump `ids
|
|
104
|
+
- `setAll(items)` — replace everything (same-order replacement does not bump `ids`; duplicate keys deduplicate — first occurrence keeps the position, last value wins)
|
|
104
105
|
- `upsert(item)` — add/replace one (appends new ids); `prepend(items)` — add to the front (scroll-up/load-older pagination)
|
|
105
106
|
- `update(id, next | updater)` / `remove(id)` / `clear()`
|
|
106
107
|
- `get(id)` / `has(id)` (untracked), `signalFor(id)` (reactive per-entity), `size`
|
|
@@ -109,7 +110,7 @@ A reactive ordered collection of keyed entities — the data half of the two-tie
|
|
|
109
110
|
|
|
110
111
|
### List rendering
|
|
111
112
|
|
|
112
|
-
- `bindList<T>(parent, source, spec): () => void` — two-tier render binding. `source` is a `ListSource<T>` = `{ ids, signalFor }`; a `Collection` satisfies it directly, and a filtered/sorted/paginated view is just `{ ids: computed(...), signalFor: collection.signalFor }` (pagination = a sliced view, not a separate primitive). One structural effect tracks `source.ids` and `reconcile`s the row list; each row owns a private effect tracking only its own entity signal, so a per-entity change repaints just that row with no structural reconcile. `spec`: `{ mount(item, id) => HTMLElement; update?(el, item, id); onRemove?(el, id) }` (`update` runs at mount and on every later change). Returns a dispose that tears down the structural effect and every row effect.
|
|
113
|
+
- `bindList<T>(parent, source, spec): () => void` — two-tier render binding. `source` is a `ListSource<T>` = `{ ids, signalFor }`; a `Collection` satisfies it directly, and a filtered/sorted/paginated view is just `{ ids: computed(...), signalFor: collection.signalFor }` (pagination = a sliced view, not a separate primitive). One structural effect tracks `source.ids` and `reconcile`s the row list; each row owns a private effect tracking only its own entity signal, so a per-entity change repaints just that row with no structural reconcile. An id whose `signalFor` is `undefined` (an inconsistent source) is skipped rather than rendered. `spec`: `{ mount(item, id) => HTMLElement; update?(el, item, id); onRemove?(el, id) }` (`update` runs at mount and on every later change). Returns a dispose that tears down the structural effect and every row effect.
|
|
113
114
|
|
|
114
115
|
### Event bus
|
|
115
116
|
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/reactive",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@eslint/js": "10.0.1",
|
|
16
|
+
"@stryker-mutator/core": "9.6.1",
|
|
17
|
+
"@stryker-mutator/typescript-checker": "9.6.1",
|
|
18
|
+
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
16
19
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
17
20
|
"@vitest/coverage-v8": "4.1.10",
|
|
18
|
-
"eslint": "10.
|
|
19
|
-
"fast-check": "4.
|
|
21
|
+
"eslint": "10.7.0",
|
|
22
|
+
"fast-check": "4.9.0",
|
|
20
23
|
"happy-dom": "20.10.6",
|
|
21
|
-
"prettier": "3.9.
|
|
24
|
+
"prettier": "3.9.5",
|
|
22
25
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
23
|
-
"typescript-eslint": "8.
|
|
26
|
+
"typescript-eslint": "8.64.0",
|
|
24
27
|
"vitest": "4.1.10"
|
|
25
28
|
},
|
|
26
29
|
"description": "Signals + DOM-reconciliation reactive core for TypeScript (zero dependencies)",
|
package/src/bind-list.ts
CHANGED
|
@@ -19,7 +19,9 @@ import { effect, type ReadonlySignal } from "./signal.js";
|
|
|
19
19
|
|
|
20
20
|
/** The minimal reactive surface `bindList` renders: a structure signal (the
|
|
21
21
|
* ordered ids) and a per-id content-signal lookup. `Collection<T>` satisfies
|
|
22
|
-
* this; so does `{ ids: computed(...), signalFor: collection.signalFor }`.
|
|
22
|
+
* this; so does `{ ids: computed(...), signalFor: collection.signalFor }`.
|
|
23
|
+
* An id with no matching signal (an inconsistent source) is skipped — no row
|
|
24
|
+
* is rendered for it until the source becomes consistent. */
|
|
23
25
|
export interface ListSource<T> {
|
|
24
26
|
readonly ids: ReadonlySignal<readonly string[]>;
|
|
25
27
|
signalFor(id: string): ReadonlySignal<T> | undefined;
|
|
@@ -51,12 +53,23 @@ export function bindList<T>(
|
|
|
51
53
|
const rowDisposers = new Map<string, () => void>();
|
|
52
54
|
|
|
53
55
|
const disposeStructural = effect(() => {
|
|
54
|
-
|
|
56
|
+
// Structure tier: re-runs on add/remove/reorder only. An id whose entity
|
|
57
|
+
// signal is missing (an inconsistent ListSource) is skipped instead of
|
|
58
|
+
// mounting a row with a manufactured `undefined as T` value; it renders
|
|
59
|
+
// once the source becomes consistent (the next `ids` change).
|
|
60
|
+
const ids = source.ids.value.filter((id) => source.signalFor(id) !== undefined);
|
|
55
61
|
reconcile<string>(parent, ids, {
|
|
56
62
|
key: (id) => id,
|
|
57
63
|
mount: (id) => {
|
|
58
64
|
const sig = source.signalFor(id);
|
|
59
|
-
|
|
65
|
+
if (sig === undefined) {
|
|
66
|
+
// Unreachable via the structural filter above (same synchronous
|
|
67
|
+
// pass); reachable only by a source whose signalFor is not stable
|
|
68
|
+
// within one read — surface that loudly instead of manufacturing
|
|
69
|
+
// an entity value. Routed to the effect error handler.
|
|
70
|
+
throw new Error(`bindList: no entity signal for id "${id}"`);
|
|
71
|
+
}
|
|
72
|
+
const el = spec.mount(sig.peek(), id);
|
|
60
73
|
if (spec.update !== undefined) {
|
|
61
74
|
const update = spec.update;
|
|
62
75
|
// content tier: isolated per-row effect; runs now (fills content)
|
package/src/collection.ts
CHANGED
|
@@ -12,7 +12,10 @@ import { SignalMap } from "./signal-map.js";
|
|
|
12
12
|
/** A reactive, ordered, keyed collection of entities of type T. */
|
|
13
13
|
export interface Collection<T> {
|
|
14
14
|
/** Replace the whole collection (e.g. initial load). Entities no longer
|
|
15
|
-
* present are dropped; the order becomes
|
|
15
|
+
* present are dropped; the order becomes `items`, deduplicated by key
|
|
16
|
+
* (first occurrence keeps the position, the last value wins — Map
|
|
17
|
+
* semantics), so the order tier can never list an id twice while the
|
|
18
|
+
* entity tier holds a single signal for it. */
|
|
16
19
|
setAll(items: readonly T[]): void;
|
|
17
20
|
/** Add a new entity or replace an existing one (keyed by `keyOf`). New
|
|
18
21
|
* entities are appended to the order. */
|
|
@@ -65,8 +68,13 @@ export function createCollection<T>(keyOf: (item: T) => string): Collection<T> {
|
|
|
65
68
|
const seen = new Set<string>();
|
|
66
69
|
for (const item of items) {
|
|
67
70
|
const id = keyOf(item);
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
// Deduplicate repeated keys (first occurrence keeps the position, the
|
|
72
|
+
// last value wins) so the structure and entity tiers cannot disagree:
|
|
73
|
+
// one signal per id, one order entry per id.
|
|
74
|
+
if (!seen.has(id)) {
|
|
75
|
+
seen.add(id);
|
|
76
|
+
ids.push(id);
|
|
77
|
+
}
|
|
70
78
|
sigs.ensure(id, item).value = item;
|
|
71
79
|
}
|
|
72
80
|
for (const id of order.peek()) {
|
package/src/reconcile-tree.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Structural tree-diff: reconcile a parent's children against new nodes.
|
|
2
2
|
// Handles attribute patching, text node updates, element reordering,
|
|
3
|
-
// and recursive child reconciliation. Keying
|
|
3
|
+
// and recursive child reconciliation. Keying: a specific `*-id` attribute
|
|
4
|
+
// wins over the generic `data-col`; duplicate-key siblings match in
|
|
5
|
+
// document order.
|
|
4
6
|
|
|
5
7
|
const handlerKeysMap = new WeakMap<HTMLElement, Set<string>>();
|
|
6
8
|
|
|
@@ -38,11 +40,19 @@ export function patch(
|
|
|
38
40
|
/** Reconcile a parent node's children against a new set of child nodes, patching in place. */
|
|
39
41
|
export function reconcileChildren(parent: Node, newChildren: Node[]): void {
|
|
40
42
|
const oldChildren = Array.from(parent.childNodes);
|
|
41
|
-
|
|
43
|
+
// Per-key FIFO queues: siblings sharing a key (e.g. two `data-col="meta"`
|
|
44
|
+
// cells in one row) match in document order (first↔first, second↔second)
|
|
45
|
+
// instead of a last-wins map entry mispairing them and churning nodes.
|
|
46
|
+
const oldByKey = new Map<string, Node[]>();
|
|
42
47
|
for (const child of oldChildren) {
|
|
43
48
|
const key = nodeKey(child);
|
|
44
49
|
if (key) {
|
|
45
|
-
oldByKey.
|
|
50
|
+
const queue = oldByKey.get(key);
|
|
51
|
+
if (queue === undefined) {
|
|
52
|
+
oldByKey.set(key, [child]);
|
|
53
|
+
} else {
|
|
54
|
+
queue.push(child);
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -54,10 +64,16 @@ export function reconcileChildren(parent: Node, newChildren: Node[]): void {
|
|
|
54
64
|
}
|
|
55
65
|
const newKey = nodeKey(newChild);
|
|
56
66
|
|
|
57
|
-
let matched
|
|
58
|
-
if (
|
|
59
|
-
oldByKey.
|
|
60
|
-
|
|
67
|
+
let matched: Node | null = null;
|
|
68
|
+
if (newKey) {
|
|
69
|
+
const queue = oldByKey.get(newKey);
|
|
70
|
+
if (queue !== undefined) {
|
|
71
|
+
matched = queue.shift() ?? null;
|
|
72
|
+
if (queue.length === 0) {
|
|
73
|
+
oldByKey.delete(newKey);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
61
77
|
// Unkeyed new node: match the next UNKEYED old node by position, advancing
|
|
62
78
|
// past keyed old nodes (which are reserved for key-based matching) and any
|
|
63
79
|
// already-consumed slots. This mirrors the documented contract ("unkeyed
|
|
@@ -139,12 +155,20 @@ function nodeKey(node: Node): string {
|
|
|
139
155
|
if (node.nodeType !== 1) {
|
|
140
156
|
return "";
|
|
141
157
|
}
|
|
158
|
+
// A specific entity id (`*-id`, e.g. data-cov-id / data-act-id) takes
|
|
159
|
+
// precedence over the generic column key (`data-col`), so an element
|
|
160
|
+
// carrying both keys by its entity identity — a generic first-in-attribute-
|
|
161
|
+
// order `data-col` must not shadow it.
|
|
162
|
+
let colKey = "";
|
|
142
163
|
for (const attr of (node as Element).attributes) {
|
|
143
|
-
if (attr.name
|
|
164
|
+
if (attr.name.endsWith("-id")) {
|
|
144
165
|
return `${attr.name}=${attr.value}`;
|
|
145
166
|
}
|
|
167
|
+
if (colKey === "" && attr.name === "data-col") {
|
|
168
|
+
colKey = `${attr.name}=${attr.value}`;
|
|
169
|
+
}
|
|
146
170
|
}
|
|
147
|
-
return
|
|
171
|
+
return colKey;
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
function patchAttrs(oldEl: HTMLElement, newEl: HTMLElement): void {
|
package/src/signal.ts
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
// Preact-style doubly-linked source/target edges, pull-based glitch-free refresh,
|
|
3
3
|
// global epoch + per-node version fast-skip, bitfield flags.
|
|
4
4
|
// GPL-3.0-or-later.
|
|
5
|
+
//
|
|
6
|
+
// Port provenance: graph/scheduler semantics verified against
|
|
7
|
+
// @preact/signals-core@1.14.4 (drift audit 2026-07-17). Deliberate deltas
|
|
8
|
+
// from upstream, pinned by tests: Object.is equality incl. NaN (upstream:
|
|
9
|
+
// `!==`); custom `equals` / `equals: false` options (upstream has none);
|
|
10
|
+
// isolate-and-continue effect error policy via setEffectErrorHandler
|
|
11
|
+
// (upstream disposes-and-rethrows); cycle guard in endBatch (upstream also
|
|
12
|
+
// guards in the signal setter). Upstream machinery NOT ported by design:
|
|
13
|
+
// watched/unwatched hooks, createModel/action (1.13+), and the 1.14.x
|
|
14
|
+
// batch-snapshot A->B->A revert optimization — the version-monotonicity bug
|
|
15
|
+
// its first release carried (preactjs/signals#947) never existed here.
|
|
16
|
+
// Re-audit on upstream releases: diff src/index.ts against this file's
|
|
17
|
+
// mechanisms and harvest engine-internal fixes (see CONTRIBUTING.md).
|
|
5
18
|
|
|
6
19
|
const SIGNAL_BRAND = Symbol("signal");
|
|
7
20
|
const COMPUTED_BRAND = Symbol("computed");
|
|
@@ -608,13 +621,19 @@ export function untracked<T>(fn: () => T): T {
|
|
|
608
621
|
}
|
|
609
622
|
}
|
|
610
623
|
|
|
611
|
-
/** Subscribe to a signal, calling cb on every change. Returns dispose function.
|
|
624
|
+
/** Subscribe to a signal, calling cb on every change. Returns dispose function.
|
|
625
|
+
* The callback runs untracked: reading other signals inside it does not add
|
|
626
|
+
* them as dependencies of the subscription (matches upstream
|
|
627
|
+
* preactjs/signals#188 semantics). */
|
|
612
628
|
export function subscribe<T>(
|
|
613
629
|
sig: Signal<T> | ReadonlySignal<T>,
|
|
614
630
|
cb: (value: T) => void,
|
|
615
631
|
): () => void {
|
|
616
632
|
return effect(() => {
|
|
617
|
-
|
|
633
|
+
const value = sig.value;
|
|
634
|
+
untracked(() => {
|
|
635
|
+
cb(value);
|
|
636
|
+
});
|
|
618
637
|
return undefined;
|
|
619
638
|
});
|
|
620
639
|
}
|
package/src/store.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// interface MyMap { count: number; name: string }
|
|
12
12
|
// const { get, set, subscribe, effect, computed, batch } = createStore<MyMap>();
|
|
13
13
|
|
|
14
|
-
import { signal, effect, batch, type Signal, type Cleanup } from "./signal.js";
|
|
14
|
+
import { signal, effect, batch, untracked, type Signal, type Cleanup } from "./signal.js";
|
|
15
15
|
|
|
16
16
|
/** A typed per-key reactive store with auto-tracked effects, computed, and batching. */
|
|
17
17
|
export interface Store<M> {
|
|
@@ -19,6 +19,14 @@ export interface Store<M> {
|
|
|
19
19
|
set<K extends keyof M & string>(key: K, value: M[K]): void;
|
|
20
20
|
subscribe<K extends keyof M & string>(key: K, cb: (value: M[K]) => void): () => void;
|
|
21
21
|
effect(fn: () => Cleanup): () => void;
|
|
22
|
+
/** Derive `outputKey` from other keys. NOT a lazy engine `computed`: this is
|
|
23
|
+
* an EAGER effect that re-runs `fn` on dependency change and WRITES the
|
|
24
|
+
* result to `outputKey`. Consequences of that shape: `fn` runs whether or
|
|
25
|
+
* not anyone reads `outputKey`; a throwing `fn` is isolated by the effect
|
|
26
|
+
* error handler (not cached and rethrown at the read site like an engine
|
|
27
|
+
* computed); `set(outputKey, …)` still works between recomputes; and a
|
|
28
|
+
* self-reading `fn` behaves as documented on the implementation below.
|
|
29
|
+
* Returns the effect's dispose function. */
|
|
22
30
|
computed<K extends keyof M & string>(outputKey: K, fn: () => M[K]): () => void;
|
|
23
31
|
batch(fn: () => void): void;
|
|
24
32
|
}
|
|
@@ -57,7 +65,11 @@ export function createStore<M>(): Store<M> {
|
|
|
57
65
|
primed = true;
|
|
58
66
|
return;
|
|
59
67
|
}
|
|
60
|
-
|
|
68
|
+
// Untracked, mirroring the engine's `subscribe`: a callback that reads
|
|
69
|
+
// other keys must not become a dependency of this subscription.
|
|
70
|
+
untracked(() => {
|
|
71
|
+
cb(v);
|
|
72
|
+
});
|
|
61
73
|
});
|
|
62
74
|
}
|
|
63
75
|
|