@weasel-js/registry 1.9.3 → 1.10.0
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 +35 -2
- package/dist/index.d.ts +50 -1
- package/dist/index.js +53 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @weasel-js/registry
|
|
2
2
|
|
|
3
|
-
Keyed registries with a uniform reflection surface: enumerate the entries, see which ones displaced others, subscribe to changes. No React, no DOM.
|
|
3
|
+
Keyed registries with a uniform reflection surface: enumerate the entries, see which ones displaced others, release any one of them, and subscribe to changes. Also the small shared pieces registries are built from: an emitter, a plain-object test, and the MIXED sentinel. No React, no DOM.
|
|
4
4
|
|
|
5
5
|
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
6
6
|
scene-graph canvas kit for React. See the
|
|
@@ -15,9 +15,42 @@ npm install @weasel-js/registry
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import {
|
|
18
|
+
import { createReflectable } from '@weasel-js/registry';
|
|
19
|
+
|
|
20
|
+
const cursors = createReflectable<string>();
|
|
21
|
+
cursors.push('pen', 'crosshair', { source: 'kit' });
|
|
22
|
+
cursors.push('pen', 'cell', { source: 'plugin' });
|
|
23
|
+
|
|
24
|
+
const [pen] = cursors.reflection.entries();
|
|
25
|
+
pen.value; // 'cell'
|
|
26
|
+
pen.shadowed; // [{ value: 'crosshair', source: 'kit' }]
|
|
27
|
+
|
|
28
|
+
cursors.release('pen', pen.live);
|
|
29
|
+
cursors.get('pen'); // 'crosshair'
|
|
19
30
|
```
|
|
20
31
|
|
|
32
|
+
## Exports
|
|
33
|
+
|
|
34
|
+
- `createReflectable<V, K>()` — a keyed store a registry embeds. It returns a
|
|
35
|
+
`Reflectable`: the write side (`push`, `set`, `release`, `delete`, `clear`,
|
|
36
|
+
`bump`) plus `reflection`, the read-only `Reflection` (`get`, `has`,
|
|
37
|
+
`entries`, `subscribe`, `getVersion`) a registry hands out. `subscribe`,
|
|
38
|
+
`getVersion`, and `entries` are shaped for `useSyncExternalStore`.
|
|
39
|
+
- `ReflectedEntry` — one key as `entries()` reports it: `key`, `value`,
|
|
40
|
+
`source`, `live` (the registrant holding the key), and `shadowed` (the
|
|
41
|
+
registrants it displaced, oldest first).
|
|
42
|
+
- `Registrant` — one `{ value, source }` claim on a key. Each is a frozen
|
|
43
|
+
object that keeps its identity, which is what `release(key, registrant)`
|
|
44
|
+
takes.
|
|
45
|
+
- `RegisterOptions` — the `{ source }` that `push` and `set` accept.
|
|
46
|
+
- `createEmitter()` and `Emitter` — a listener set with `subscribe`, `emit`,
|
|
47
|
+
and `size`. A listener that throws is reported to `console.error` and does
|
|
48
|
+
not stop the rest.
|
|
49
|
+
- `isPlainObject(value)` and `isPlainPrototype(proto)` — plain-object tests
|
|
50
|
+
that hold for objects from another realm.
|
|
51
|
+
- `MIXED` and `Mixed` — the sentinel for a value aggregated across sources
|
|
52
|
+
that disagree.
|
|
53
|
+
|
|
21
54
|
## License
|
|
22
55
|
|
|
23
56
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ interface Registrant<V> {
|
|
|
6
6
|
/** One key as a reflection reports it. */
|
|
7
7
|
interface ReflectedEntry<V, K = string> extends Registrant<V> {
|
|
8
8
|
readonly key: K;
|
|
9
|
+
/** The registrant whose `value` and `source` this entry carries. The same
|
|
10
|
+
* object across snapshots, and the one `shadowed` lists once it is
|
|
11
|
+
* displaced, so it identifies the registrant to {@link Reflectable.release}. */
|
|
12
|
+
readonly live: Registrant<V>;
|
|
9
13
|
/** The registrants this one displaced, oldest first. Non-empty means two
|
|
10
14
|
* registrants claimed one key — a conflict, resolved newest-wins. */
|
|
11
15
|
readonly shadowed: readonly Registrant<V>[];
|
|
@@ -47,6 +51,11 @@ interface Reflectable<V, K = string> extends Reflection<V, K> {
|
|
|
47
51
|
* registries where a second registration means "newer version", not
|
|
48
52
|
* "override". Records no conflict. */
|
|
49
53
|
set(key: K, value: V, options?: RegisterOptions): void;
|
|
54
|
+
/** Remove exactly `registrant` — one a reflection reported under `key`, live
|
|
55
|
+
* or shadowed, written by `push` or by `set`. Removing the live one uncovers
|
|
56
|
+
* the one beneath. Returns whether it was there; the release `push` returned
|
|
57
|
+
* for it does nothing afterward, and the reverse. */
|
|
58
|
+
release(key: K, registrant: Registrant<V>): boolean;
|
|
50
59
|
/** Drop every registrant of `key`. Returns whether there was one. */
|
|
51
60
|
delete(key: K): boolean;
|
|
52
61
|
clear(): void;
|
|
@@ -63,4 +72,44 @@ interface Reflectable<V, K = string> extends Reflection<V, K> {
|
|
|
63
72
|
*/
|
|
64
73
|
declare function createReflectable<V, K = string>(): Reflectable<V, K>;
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
/** A set of listeners and the one way the kit notifies them. See {@link createEmitter}. */
|
|
76
|
+
interface Emitter<TArgs extends unknown[] = []> {
|
|
77
|
+
/** Add `listener`; a listener already held is held once. Returns the unsubscribe. */
|
|
78
|
+
subscribe(listener: (...args: TArgs) => void): () => void;
|
|
79
|
+
/** Call every listener held when the call began. */
|
|
80
|
+
emit(...args: TArgs): void;
|
|
81
|
+
/** How many listeners are held. */
|
|
82
|
+
readonly size: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A subscribe/notify pair with stated semantics, for a store to build on
|
|
86
|
+
* instead of keeping its own listener set:
|
|
87
|
+
*
|
|
88
|
+
* - `emit` walks the listeners held when it began, in subscription order. One
|
|
89
|
+
* that subscribes during a pass waits for the next; one unsubscribed before
|
|
90
|
+
* its turn is not called.
|
|
91
|
+
* - A listener that throws does not stop the others, and does not throw into
|
|
92
|
+
* whatever caused the notify: the error goes to `console.error`.
|
|
93
|
+
*/
|
|
94
|
+
declare function createEmitter<TArgs extends unknown[] = []>(): Emitter<TArgs>;
|
|
95
|
+
|
|
96
|
+
/** Whether `proto` is the prototype a plain object has: `null`, or some
|
|
97
|
+
* realm's `Object.prototype`. Comparing against this realm's alone misses
|
|
98
|
+
* objects made in an iframe, a VM context, or by `structuredClone` under jsdom. */
|
|
99
|
+
declare function isPlainPrototype(proto: object | null): boolean;
|
|
100
|
+
/** Whether `value` is a plain object — a literal or `Object.create(null)` — from any realm. */
|
|
101
|
+
declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The kit-wide "these values disagree" sentinel. Used wherever a value is
|
|
105
|
+
* aggregated across more than one source and the sources don't agree — e.g.
|
|
106
|
+
* a UI panel aggregating a property across a multi-node selection, or text
|
|
107
|
+
* styling aggregated across the runs in a character range. One symbol, one
|
|
108
|
+
* `isMixed` check, regardless of what's being aggregated.
|
|
109
|
+
*/
|
|
110
|
+
declare const MIXED: unique symbol;
|
|
111
|
+
/** The type of {@link MIXED}. Property panels use it to say "the selection
|
|
112
|
+
* does not agree on this value" without colliding with any real value. */
|
|
113
|
+
type Mixed = typeof MIXED;
|
|
114
|
+
|
|
115
|
+
export { type Emitter, MIXED, type Mixed, type Reflectable, type ReflectedEntry, type Reflection, type RegisterOptions, type Registrant, createEmitter, createReflectable, isPlainObject, isPlainPrototype };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
|
+
// src/emitter.ts
|
|
2
|
+
function createEmitter() {
|
|
3
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4
|
+
return {
|
|
5
|
+
subscribe(listener) {
|
|
6
|
+
listeners.add(listener);
|
|
7
|
+
return () => {
|
|
8
|
+
listeners.delete(listener);
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
emit(...args) {
|
|
12
|
+
for (const listener of [...listeners]) {
|
|
13
|
+
if (!listeners.has(listener)) continue;
|
|
14
|
+
try {
|
|
15
|
+
listener(...args);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error("weasel: a subscriber threw", err);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
get size() {
|
|
22
|
+
return listeners.size;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
1
27
|
// src/reflectable.ts
|
|
2
28
|
function createReflectable() {
|
|
3
29
|
const stacks = /* @__PURE__ */ new Map();
|
|
4
|
-
const listeners =
|
|
30
|
+
const listeners = createEmitter();
|
|
5
31
|
let version = 0;
|
|
6
32
|
let snapshot = null;
|
|
7
33
|
function changed() {
|
|
8
34
|
version++;
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
listener();
|
|
12
|
-
} catch (err) {
|
|
13
|
-
console.error("weasel registry: subscriber threw", err);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
35
|
+
listeners.emit();
|
|
16
36
|
}
|
|
17
37
|
const reflection = {
|
|
18
38
|
get: (key) => stacks.get(key)?.at(-1)?.value,
|
|
@@ -24,6 +44,7 @@ function createReflectable() {
|
|
|
24
44
|
const live = stack.at(-1);
|
|
25
45
|
entries.push(Object.freeze({
|
|
26
46
|
key,
|
|
47
|
+
live,
|
|
27
48
|
value: live.value,
|
|
28
49
|
source: live.source,
|
|
29
50
|
shadowed: Object.freeze(stack.slice(0, -1))
|
|
@@ -32,33 +53,30 @@ function createReflectable() {
|
|
|
32
53
|
snapshot = { version, entries: Object.freeze(entries) };
|
|
33
54
|
return snapshot.entries;
|
|
34
55
|
},
|
|
35
|
-
subscribe
|
|
36
|
-
listeners.add(listener);
|
|
37
|
-
return () => {
|
|
38
|
-
listeners.delete(listener);
|
|
39
|
-
};
|
|
40
|
-
},
|
|
56
|
+
subscribe: listeners.subscribe,
|
|
41
57
|
getVersion: () => version
|
|
42
58
|
};
|
|
59
|
+
function release(key, registrant) {
|
|
60
|
+
const stack = stacks.get(key);
|
|
61
|
+
const i = stack ? stack.lastIndexOf(registrant) : -1;
|
|
62
|
+
if (i === -1) return false;
|
|
63
|
+
stack.splice(i, 1);
|
|
64
|
+
if (stack.length === 0) stacks.delete(key);
|
|
65
|
+
changed();
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
43
68
|
return {
|
|
44
69
|
...reflection,
|
|
45
70
|
reflection,
|
|
71
|
+
release,
|
|
46
72
|
push(key, value, options) {
|
|
47
73
|
const registrant = Object.freeze({ value, source: options?.source });
|
|
48
74
|
const stack = stacks.get(key);
|
|
49
75
|
if (stack) stack.push(registrant);
|
|
50
76
|
else stacks.set(key, [registrant]);
|
|
51
77
|
changed();
|
|
52
|
-
let released = false;
|
|
53
78
|
return () => {
|
|
54
|
-
|
|
55
|
-
released = true;
|
|
56
|
-
const cur = stacks.get(key);
|
|
57
|
-
const i = cur ? cur.lastIndexOf(registrant) : -1;
|
|
58
|
-
if (i === -1) return;
|
|
59
|
-
cur.splice(i, 1);
|
|
60
|
-
if (cur.length === 0) stacks.delete(key);
|
|
61
|
-
changed();
|
|
79
|
+
release(key, registrant);
|
|
62
80
|
};
|
|
63
81
|
},
|
|
64
82
|
set(key, value, options) {
|
|
@@ -82,6 +100,17 @@ function createReflectable() {
|
|
|
82
100
|
};
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
|
|
103
|
+
// src/isPlainObject.ts
|
|
104
|
+
function isPlainPrototype(proto) {
|
|
105
|
+
return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null;
|
|
106
|
+
}
|
|
107
|
+
function isPlainObject(value) {
|
|
108
|
+
return typeof value === "object" && value !== null && isPlainPrototype(Object.getPrototypeOf(value));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/mixed.ts
|
|
112
|
+
var MIXED = /* @__PURE__ */ Symbol("weasel:mixed");
|
|
113
|
+
|
|
114
|
+
export { MIXED, createEmitter, createReflectable, isPlainObject, isPlainPrototype };
|
|
86
115
|
//# sourceMappingURL=index.js.map
|
|
87
116
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/reflectable.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** One registrant of a key: its value and, when the registrant named one, where it came from. */\nexport interface Registrant<V> {\n readonly value: V;\n readonly source: string | undefined;\n}\n\n/** One key as a reflection reports it. */\nexport interface ReflectedEntry<V, K = string> extends Registrant<V> {\n readonly key: K;\n /** The registrants this one displaced, oldest first. Non-empty means two\n * registrants claimed one key — a conflict, resolved newest-wins. */\n readonly shadowed: readonly Registrant<V>[];\n}\n\n/**\n * The read-only face of a registry: what is registered, what displaced what,\n * and a way to hear about changes. Every kit registry that embeds a\n * {@link Reflectable} hands this out, so a debug overlay or a conflict check\n * can walk any of them the same way.\n *\n * `subscribe` and `getVersion` are shaped for `useSyncExternalStore`; so is\n * `entries`, which returns the same frozen array until the next change.\n */\nexport interface Reflection<V, K = string> {\n get(key: K): V | undefined;\n has(key: K): boolean;\n /** Every key's live registrant, in the order each key was first registered. */\n entries(): readonly ReflectedEntry<V, K>[];\n /** Called after every change. Returns the unsubscribe. */\n subscribe(listener: () => void): () => void;\n /** Advances on every change. */\n getVersion(): number;\n}\n\n/** Options for {@link Reflectable.push} and {@link Reflectable.set}. */\nexport interface RegisterOptions {\n /** Who registered this — `'kit'`, a package, a plugin id. Reported, never interpreted. */\n source?: string;\n}\n\n/**\n * A keyed store that keeps its own {@link Reflection}. The registry owning it\n * writes through `push` / `set` / `delete` and hands out `reflection`.\n */\nexport interface Reflectable<V, K = string> extends Reflection<V, K> {\n /** Register `value` over whatever `key` holds and return its release.\n * Registrants stack, newest live: releasing the live one uncovers the one\n * it displaced, and releasing a displaced one removes only it. Double-release safe. */\n push(key: K, value: V, options?: RegisterOptions): () => void;\n /** Replace `key`'s live registrant in place, the way `Map.set` does — for\n * registries where a second registration means \"newer version\", not\n * \"override\". Records no conflict. */\n set(key: K, value: V, options?: RegisterOptions): void;\n /** Drop every registrant of `key`. Returns whether there was one. */\n delete(key: K): boolean;\n clear(): void;\n /** Announce a change the store cannot see — a registered value mutated in place. */\n bump(): void;\n /** This store as a {@link Reflection}, with no write methods on it. */\n readonly reflection: Reflection<V, K>;\n}\n\n/**\n * Create a keyed store with a uniform reflection surface.\n *\n * Deliberately only the cross-cutting part of a registry: validation, lookup\n * fallbacks and lifecycle stay with the registry that embeds it.\n */\nexport function createReflectable<V, K = string>(): Reflectable<V, K> {\n const stacks = new Map<K, Registrant<V>[]>();\n const listeners = new Set<() => void>();\n let version = 0;\n let snapshot: { version: number; entries: readonly ReflectedEntry<V, K>[] } | null = null;\n\n function changed(): void {\n version++;\n for (const listener of listeners) {\n try {\n listener();\n } catch (err) {\n console.error('weasel registry: subscriber threw', err);\n }\n }\n }\n\n const reflection: Reflection<V, K> = {\n get: (key) => stacks.get(key)?.at(-1)?.value,\n has: (key) => stacks.has(key),\n entries() {\n if (snapshot?.version === version) return snapshot.entries;\n const entries: ReflectedEntry<V, K>[] = [];\n for (const [key, stack] of stacks) {\n const live = stack.at(-1)!;\n entries.push(Object.freeze({\n key, value: live.value, source: live.source, shadowed: Object.freeze(stack.slice(0, -1)),\n }));\n }\n snapshot = { version, entries: Object.freeze(entries) };\n return snapshot.entries;\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n getVersion: () => version,\n };\n\n return {\n ...reflection,\n reflection,\n push(key, value, options) {\n const registrant: Registrant<V> = Object.freeze({ value, source: options?.source });\n const stack = stacks.get(key);\n if (stack) stack.push(registrant);\n else stacks.set(key, [registrant]);\n changed();\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const cur = stacks.get(key);\n const i = cur ? cur.lastIndexOf(registrant) : -1;\n if (i === -1) return;\n cur!.splice(i, 1);\n if (cur!.length === 0) stacks.delete(key);\n changed();\n };\n },\n set(key, value, options) {\n const registrant: Registrant<V> = Object.freeze({ value, source: options?.source });\n const stack = stacks.get(key);\n if (stack) stack[stack.length - 1] = registrant;\n else stacks.set(key, [registrant]);\n changed();\n },\n delete(key) {\n if (!stacks.delete(key)) return false;\n changed();\n return true;\n },\n clear() {\n if (stacks.size === 0) return;\n stacks.clear();\n changed();\n },\n bump: changed,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/emitter.ts","../src/reflectable.ts","../src/isPlainObject.ts","../src/mixed.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** A set of listeners and the one way the kit notifies them. See {@link createEmitter}. */\nexport interface Emitter<TArgs extends unknown[] = []> {\n /** Add `listener`; a listener already held is held once. Returns the unsubscribe. */\n subscribe(listener: (...args: TArgs) => void): () => void;\n /** Call every listener held when the call began. */\n emit(...args: TArgs): void;\n /** How many listeners are held. */\n readonly size: number;\n}\n\n/**\n * A subscribe/notify pair with stated semantics, for a store to build on\n * instead of keeping its own listener set:\n *\n * - `emit` walks the listeners held when it began, in subscription order. One\n * that subscribes during a pass waits for the next; one unsubscribed before\n * its turn is not called.\n * - A listener that throws does not stop the others, and does not throw into\n * whatever caused the notify: the error goes to `console.error`.\n */\nexport function createEmitter<TArgs extends unknown[] = []>(): Emitter<TArgs> {\n const listeners = new Set<(...args: TArgs) => void>();\n return {\n subscribe(listener) {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n emit(...args) {\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue;\n try {\n listener(...args);\n } catch (err) {\n console.error('weasel: a subscriber threw', err);\n }\n }\n },\n get size() {\n return listeners.size;\n },\n };\n}\n","import { createEmitter } from './emitter';\n\n/** One registrant of a key: its value and, when the registrant named one, where it came from. */\nexport interface Registrant<V> {\n readonly value: V;\n readonly source: string | undefined;\n}\n\n/** One key as a reflection reports it. */\nexport interface ReflectedEntry<V, K = string> extends Registrant<V> {\n readonly key: K;\n /** The registrant whose `value` and `source` this entry carries. The same\n * object across snapshots, and the one `shadowed` lists once it is\n * displaced, so it identifies the registrant to {@link Reflectable.release}. */\n readonly live: Registrant<V>;\n /** The registrants this one displaced, oldest first. Non-empty means two\n * registrants claimed one key — a conflict, resolved newest-wins. */\n readonly shadowed: readonly Registrant<V>[];\n}\n\n/**\n * The read-only face of a registry: what is registered, what displaced what,\n * and a way to hear about changes. Every kit registry that embeds a\n * {@link Reflectable} hands this out, so a debug overlay or a conflict check\n * can walk any of them the same way.\n *\n * `subscribe` and `getVersion` are shaped for `useSyncExternalStore`; so is\n * `entries`, which returns the same frozen array until the next change.\n */\nexport interface Reflection<V, K = string> {\n get(key: K): V | undefined;\n has(key: K): boolean;\n /** Every key's live registrant, in the order each key was first registered. */\n entries(): readonly ReflectedEntry<V, K>[];\n /** Called after every change. Returns the unsubscribe. */\n subscribe(listener: () => void): () => void;\n /** Advances on every change. */\n getVersion(): number;\n}\n\n/** Options for {@link Reflectable.push} and {@link Reflectable.set}. */\nexport interface RegisterOptions {\n /** Who registered this — `'kit'`, a package, a plugin id. Reported, never interpreted. */\n source?: string;\n}\n\n/**\n * A keyed store that keeps its own {@link Reflection}. The registry owning it\n * writes through `push` / `set` / `delete` and hands out `reflection`.\n */\nexport interface Reflectable<V, K = string> extends Reflection<V, K> {\n /** Register `value` over whatever `key` holds and return its release.\n * Registrants stack, newest live: releasing the live one uncovers the one\n * it displaced, and releasing a displaced one removes only it. Double-release safe. */\n push(key: K, value: V, options?: RegisterOptions): () => void;\n /** Replace `key`'s live registrant in place, the way `Map.set` does — for\n * registries where a second registration means \"newer version\", not\n * \"override\". Records no conflict. */\n set(key: K, value: V, options?: RegisterOptions): void;\n /** Remove exactly `registrant` — one a reflection reported under `key`, live\n * or shadowed, written by `push` or by `set`. Removing the live one uncovers\n * the one beneath. Returns whether it was there; the release `push` returned\n * for it does nothing afterward, and the reverse. */\n release(key: K, registrant: Registrant<V>): boolean;\n /** Drop every registrant of `key`. Returns whether there was one. */\n delete(key: K): boolean;\n clear(): void;\n /** Announce a change the store cannot see — a registered value mutated in place. */\n bump(): void;\n /** This store as a {@link Reflection}, with no write methods on it. */\n readonly reflection: Reflection<V, K>;\n}\n\n/**\n * Create a keyed store with a uniform reflection surface.\n *\n * Deliberately only the cross-cutting part of a registry: validation, lookup\n * fallbacks and lifecycle stay with the registry that embeds it.\n */\nexport function createReflectable<V, K = string>(): Reflectable<V, K> {\n const stacks = new Map<K, Registrant<V>[]>();\n const listeners = createEmitter();\n let version = 0;\n let snapshot: { version: number; entries: readonly ReflectedEntry<V, K>[] } | null = null;\n\n function changed(): void {\n version++;\n listeners.emit();\n }\n\n const reflection: Reflection<V, K> = {\n get: (key) => stacks.get(key)?.at(-1)?.value,\n has: (key) => stacks.has(key),\n entries() {\n if (snapshot?.version === version) return snapshot.entries;\n const entries: ReflectedEntry<V, K>[] = [];\n for (const [key, stack] of stacks) {\n const live = stack.at(-1)!;\n entries.push(Object.freeze({\n key, live, value: live.value, source: live.source, shadowed: Object.freeze(stack.slice(0, -1)),\n }));\n }\n snapshot = { version, entries: Object.freeze(entries) };\n return snapshot.entries;\n },\n subscribe: listeners.subscribe,\n getVersion: () => version,\n };\n\n function release(key: K, registrant: Registrant<V>): boolean {\n const stack = stacks.get(key);\n const i = stack ? stack.lastIndexOf(registrant) : -1;\n if (i === -1) return false;\n stack!.splice(i, 1);\n if (stack!.length === 0) stacks.delete(key);\n changed();\n return true;\n }\n\n return {\n ...reflection,\n reflection,\n release,\n push(key, value, options) {\n const registrant: Registrant<V> = Object.freeze({ value, source: options?.source });\n const stack = stacks.get(key);\n if (stack) stack.push(registrant);\n else stacks.set(key, [registrant]);\n changed();\n return () => {\n release(key, registrant);\n };\n },\n set(key, value, options) {\n const registrant: Registrant<V> = Object.freeze({ value, source: options?.source });\n const stack = stacks.get(key);\n if (stack) stack[stack.length - 1] = registrant;\n else stacks.set(key, [registrant]);\n changed();\n },\n delete(key) {\n if (!stacks.delete(key)) return false;\n changed();\n return true;\n },\n clear() {\n if (stacks.size === 0) return;\n stacks.clear();\n changed();\n },\n bump: changed,\n };\n}\n","/** Whether `proto` is the prototype a plain object has: `null`, or some\n * realm's `Object.prototype`. Comparing against this realm's alone misses\n * objects made in an iframe, a VM context, or by `structuredClone` under jsdom. */\nexport function isPlainPrototype(proto: object | null): boolean {\n return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null;\n}\n\n/** Whether `value` is a plain object — a literal or `Object.create(null)` — from any realm. */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && isPlainPrototype(Object.getPrototypeOf(value));\n}\n","/**\n * The kit-wide \"these values disagree\" sentinel. Used wherever a value is\n * aggregated across more than one source and the sources don't agree — e.g.\n * a UI panel aggregating a property across a multi-node selection, or text\n * styling aggregated across the runs in a character range. One symbol, one\n * `isMixed` check, regardless of what's being aggregated.\n */\nexport const MIXED: unique symbol = Symbol('weasel:mixed');\n/** The type of {@link MIXED}. Property panels use it to say \"the selection\n * does not agree on this value\" without colliding with any real value. */\nexport type Mixed = typeof MIXED;\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/registry",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Keyed registries with a uniform reflection surface: enumerate the entries, see which ones displaced others, subscribe to changes. No React, no DOM.",
|
|
3
|
+
"version": "1.10.0",
|
|
4
|
+
"description": "Keyed registries with a uniform reflection surface: enumerate the entries, see which ones displaced others, release any one of them, and subscribe to changes. Also the small shared pieces registries are built from: an emitter, a plain-object test, and the MIXED sentinel. No React, no DOM.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|