@wildwinter/scoperegistry 0.1.0 → 0.2.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 +21 -0
- package/dist/index.cjs +177 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -6
- package/dist/index.d.ts +105 -6
- package/dist/index.js +176 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,27 @@ another engine.
|
|
|
27
27
|
settable per scope (a resolver with no `set`) and per property
|
|
28
28
|
(`writable: false`), enforced by `set`.
|
|
29
29
|
|
|
30
|
+
Since 0.2.0 this package is also the **state kernel** the Patter and
|
|
31
|
+
Storylet Engine runtime families share (their "one properties implementer"):
|
|
32
|
+
|
|
33
|
+
- **`PropertyBag`** is a first-class citizen: typed declarations + defaults,
|
|
34
|
+
the firing rule (engine writes notify `subscribe`rs; host writes pass
|
|
35
|
+
`{ silent: true, reason }` and reach only the always-on `onAudit` hook),
|
|
36
|
+
examiner `rows()`, one sanctioned `clone()` door, in-place `reseed`, and
|
|
37
|
+
bare-value `save()`/`load()`. Name normalisation is a policy: lowercase by
|
|
38
|
+
default, or pass `{ normalise }` (a case-significant product passes
|
|
39
|
+
identity).
|
|
40
|
+
- Owned registry scopes are bags: `ownedBag(token)` exposes them, and
|
|
41
|
+
`mountOwned(token, bag)` attaches a bag another holder owns (the shared
|
|
42
|
+
state container for a mixed two-engine game).
|
|
43
|
+
- **`listProperties()`** returns examiner rows across owned scopes and
|
|
44
|
+
declared foreign scopes, for the shared in-engine property panels.
|
|
45
|
+
- **`saveFragment()`/`loadFragment()`** speak a versioned
|
|
46
|
+
`OwnedStateFragment` (`{ version, scopes }`), the one serialisation shape
|
|
47
|
+
both products' save envelopes embed when they adopt the kernel.
|
|
48
|
+
`save()`/`load()` keep the bare 0.1.x shape, so existing consumers' save
|
|
49
|
+
formats are untouched.
|
|
50
|
+
|
|
30
51
|
```ts
|
|
31
52
|
import { ScopeRegistry } from "@wildwinter/scoperegistry";
|
|
32
53
|
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,106 @@ function readScopeRegistrySpec(source) {
|
|
|
20
20
|
}
|
|
21
21
|
return spec;
|
|
22
22
|
}
|
|
23
|
+
var PropertyBag = class _PropertyBag {
|
|
24
|
+
/** The live values record (stable identity across reseed, so an
|
|
25
|
+
* EvalContext built over it stays valid). Read-path for evaluation;
|
|
26
|
+
* writes go through `set` so the firing rule applies. */
|
|
27
|
+
values = {};
|
|
28
|
+
decls = /* @__PURE__ */ new Map();
|
|
29
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
30
|
+
auditors = /* @__PURE__ */ new Set();
|
|
31
|
+
/** Name normalisation policy: lowercase by default (the registry's
|
|
32
|
+
* long-standing contract); a product whose names are case-significant
|
|
33
|
+
* passes identity. */
|
|
34
|
+
norm;
|
|
35
|
+
constructor(declarations = [], opts) {
|
|
36
|
+
this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
|
|
37
|
+
this.seed(declarations);
|
|
38
|
+
}
|
|
39
|
+
seed(declarations) {
|
|
40
|
+
for (const d of declarations) {
|
|
41
|
+
const name = this.norm(d.name);
|
|
42
|
+
this.decls.set(name, d);
|
|
43
|
+
this.values[name] = structuredClone(d.default ?? defaultFor(d));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
get(name) {
|
|
47
|
+
return this.values[this.norm(name)];
|
|
48
|
+
}
|
|
49
|
+
/** Write a property. Engine writes (the default) notify subscribers;
|
|
50
|
+
* pass `silent: true` for a host write, which reaches only the audit
|
|
51
|
+
* hook. Throws on a read-only property. Returns the change. */
|
|
52
|
+
set(name, value, opts) {
|
|
53
|
+
const n = this.norm(name);
|
|
54
|
+
if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
55
|
+
const change = {
|
|
56
|
+
name: n,
|
|
57
|
+
prev: this.values[n],
|
|
58
|
+
next: value,
|
|
59
|
+
silent: opts?.silent ?? false,
|
|
60
|
+
reason: opts?.reason
|
|
61
|
+
};
|
|
62
|
+
this.values[n] = value;
|
|
63
|
+
for (const audit of this.auditors) audit(change);
|
|
64
|
+
if (!change.silent) for (const fn of this.subscribers) fn(change);
|
|
65
|
+
return change;
|
|
66
|
+
}
|
|
67
|
+
/** Notified of engine (non-silent) writes. Returns the unsubscribe. */
|
|
68
|
+
subscribe(fn) {
|
|
69
|
+
this.subscribers.add(fn);
|
|
70
|
+
return () => this.subscribers.delete(fn);
|
|
71
|
+
}
|
|
72
|
+
/** Notified of EVERY write, silent or not. Returns the unsubscribe. */
|
|
73
|
+
onAudit(fn) {
|
|
74
|
+
this.auditors.add(fn);
|
|
75
|
+
return () => this.auditors.delete(fn);
|
|
76
|
+
}
|
|
77
|
+
/** Examiner rows: the declared surface only (stray values are storage,
|
|
78
|
+
* not surface). */
|
|
79
|
+
rows() {
|
|
80
|
+
return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name));
|
|
81
|
+
}
|
|
82
|
+
declarations() {
|
|
83
|
+
return [...this.decls.values()];
|
|
84
|
+
}
|
|
85
|
+
/** The one sanctioned copy door: values deep-copied, declarations
|
|
86
|
+
* duplicated, the normalisation policy carried, subscriptions NOT
|
|
87
|
+
* carried. */
|
|
88
|
+
clone() {
|
|
89
|
+
const c = new _PropertyBag([], { normalise: this.norm });
|
|
90
|
+
c.decls = new Map(this.decls);
|
|
91
|
+
Object.assign(c.values, structuredClone(this.values));
|
|
92
|
+
return c;
|
|
93
|
+
}
|
|
94
|
+
/** Clear and re-seed from new declarations, in place (the values record
|
|
95
|
+
* keeps its identity, so contexts built over it stay valid). */
|
|
96
|
+
reseed(declarations) {
|
|
97
|
+
for (const k of Object.keys(this.values)) delete this.values[k];
|
|
98
|
+
this.decls.clear();
|
|
99
|
+
this.seed(declarations);
|
|
100
|
+
}
|
|
101
|
+
/** Bare values, ready to embed in a product's save. */
|
|
102
|
+
save() {
|
|
103
|
+
return structuredClone(this.values);
|
|
104
|
+
}
|
|
105
|
+
/** Lay saved values over the current ones (call after a fresh seed:
|
|
106
|
+
* orphans land as strays, new declarations keep their defaults; the
|
|
107
|
+
* product decides whether to prune). Does not fire events. */
|
|
108
|
+
load(values) {
|
|
109
|
+
for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function rowFor(d, value, writable, name) {
|
|
113
|
+
return {
|
|
114
|
+
name: name ?? d.name.toLowerCase(),
|
|
115
|
+
type: d.type,
|
|
116
|
+
value,
|
|
117
|
+
default: d.default ?? defaultFor(d),
|
|
118
|
+
...d.values !== void 0 ? { values: d.values } : {},
|
|
119
|
+
writable: writable ?? d.writable ?? true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
var SAVE_FRAGMENT_VERSION = 1;
|
|
23
123
|
var ScopeRegistry = class {
|
|
24
124
|
scopes = /* @__PURE__ */ new Map();
|
|
25
125
|
/**
|
|
@@ -28,17 +128,24 @@ var ScopeRegistry = class {
|
|
|
28
128
|
* type-checked (declarations) and serialized by `save`/`load`.
|
|
29
129
|
*/
|
|
30
130
|
defineOwned(token, declarations) {
|
|
131
|
+
return this.mountOwned(token, new PropertyBag(declarations));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Attach an EXISTING bag as an owned scope - the shared-container move: a
|
|
135
|
+
* host (or the other product) holds the bag; this registry reads, writes
|
|
136
|
+
* and lists it like its own, but the holder saves it.
|
|
137
|
+
*/
|
|
138
|
+
mountOwned(token, bag) {
|
|
31
139
|
this.assertFree(token);
|
|
32
|
-
|
|
33
|
-
const decls = /* @__PURE__ */ new Map();
|
|
34
|
-
for (const d of declarations) {
|
|
35
|
-
const name = d.name.toLowerCase();
|
|
36
|
-
decls.set(name, d);
|
|
37
|
-
bag[name] = d.default ?? defaultFor(d);
|
|
38
|
-
}
|
|
39
|
-
this.scopes.set(token, { kind: "owned", bag, decls });
|
|
140
|
+
this.scopes.set(token, { kind: "owned", bag });
|
|
40
141
|
return this;
|
|
41
142
|
}
|
|
143
|
+
/** An owned scope's bag (subscribe, audit, rows live there). */
|
|
144
|
+
ownedBag(token) {
|
|
145
|
+
const e = this.scopes.get(token);
|
|
146
|
+
if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
|
|
147
|
+
return e.bag;
|
|
148
|
+
}
|
|
42
149
|
/**
|
|
43
150
|
* Re-initialise an existing **owned** scope's bag from new declarations,
|
|
44
151
|
* clearing its current values. For scope-local state that resets on a context
|
|
@@ -47,15 +154,7 @@ var ScopeRegistry = class {
|
|
|
47
154
|
* registry stays valid.
|
|
48
155
|
*/
|
|
49
156
|
reseedOwned(token, declarations) {
|
|
50
|
-
|
|
51
|
-
if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
|
|
52
|
-
for (const k of Object.keys(e.bag)) delete e.bag[k];
|
|
53
|
-
e.decls.clear();
|
|
54
|
-
for (const d of declarations) {
|
|
55
|
-
const name = d.name.toLowerCase();
|
|
56
|
-
e.decls.set(name, d);
|
|
57
|
-
e.bag[name] = d.default ?? defaultFor(d);
|
|
58
|
-
}
|
|
157
|
+
this.ownedBag(token).reseed(declarations);
|
|
59
158
|
return this;
|
|
60
159
|
}
|
|
61
160
|
/**
|
|
@@ -78,23 +177,49 @@ var ScopeRegistry = class {
|
|
|
78
177
|
get(scope, name) {
|
|
79
178
|
const e = this.scopes.get(scope);
|
|
80
179
|
if (!e) return void 0;
|
|
81
|
-
|
|
82
|
-
return e.kind === "owned" ? e.bag[n] : e.resolver.get(n);
|
|
180
|
+
return e.kind === "owned" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());
|
|
83
181
|
}
|
|
84
|
-
/** Write a property
|
|
182
|
+
/** Write a property (an ENGINE write: the bag's subscribers fire; use
|
|
183
|
+
* the bag directly for silent host writes). Throws on an unknown or
|
|
184
|
+
* read-only scope/property. */
|
|
85
185
|
set(scope, name, value) {
|
|
86
186
|
const e = this.scopes.get(scope);
|
|
87
187
|
if (!e) throw new Error(`unknown scope '@${scope}'`);
|
|
188
|
+
if (e.kind === "owned") {
|
|
189
|
+
try {
|
|
190
|
+
e.bag.set(name, value);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new Error(`'@${scope}.${name}' is read-only`);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
88
196
|
const n = name.toLowerCase();
|
|
89
|
-
if (!this.
|
|
90
|
-
|
|
91
|
-
else e.resolver.set(n, value);
|
|
197
|
+
if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);
|
|
198
|
+
e.resolver.set(n, value);
|
|
92
199
|
}
|
|
93
|
-
|
|
94
|
-
if (e.kind === "owned") return e.decls.get(name)?.writable ?? true;
|
|
200
|
+
foreignWritable(e, name) {
|
|
95
201
|
if (!e.resolver.set) return false;
|
|
96
202
|
return e.decls.get(name)?.writable ?? e.scopeWritable;
|
|
97
203
|
}
|
|
204
|
+
/** Examiner rows across every scope with a declared surface: owned bags
|
|
205
|
+
* first, then declared foreign scopes (values read through, writability
|
|
206
|
+
* reflecting the resolver). Opaque foreign scopes are not listed. */
|
|
207
|
+
listProperties() {
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const [token, e] of this.scopes) {
|
|
210
|
+
if (e.kind === "owned") {
|
|
211
|
+
for (const row of e.bag.rows()) out.push({ scope: token, ...row });
|
|
212
|
+
} else {
|
|
213
|
+
for (const d of e.decls.values()) {
|
|
214
|
+
out.push({
|
|
215
|
+
scope: token,
|
|
216
|
+
...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()))
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
98
223
|
/**
|
|
99
224
|
* Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
|
|
100
225
|
* bags, foreign scopes as their resolvers. `host` carries dialect-function
|
|
@@ -103,7 +228,7 @@ var ScopeRegistry = class {
|
|
|
103
228
|
toEvalContext(host) {
|
|
104
229
|
const scopes = {};
|
|
105
230
|
for (const [token, e] of this.scopes) {
|
|
106
|
-
scopes[token] = e.kind === "owned" ? e.bag : e.resolver;
|
|
231
|
+
scopes[token] = e.kind === "owned" ? e.bag.values : e.resolver;
|
|
107
232
|
}
|
|
108
233
|
return { scopes, host };
|
|
109
234
|
}
|
|
@@ -115,25 +240,43 @@ var ScopeRegistry = class {
|
|
|
115
240
|
toSchema() {
|
|
116
241
|
const properties = /* @__PURE__ */ new Map();
|
|
117
242
|
for (const [token, e] of this.scopes) {
|
|
118
|
-
|
|
243
|
+
const decls = e.kind === "owned" ? e.bag.declarations() : [...e.decls.values()];
|
|
244
|
+
if (decls.length === 0) continue;
|
|
119
245
|
const m = /* @__PURE__ */ new Map();
|
|
120
|
-
for (const
|
|
246
|
+
for (const d of decls) m.set(d.name.toLowerCase(), { type: d.type, enumValues: d.values });
|
|
121
247
|
properties.set(token, m);
|
|
122
248
|
}
|
|
123
249
|
return { properties };
|
|
124
250
|
}
|
|
125
|
-
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
251
|
+
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
252
|
+
* host-saved), as bare bags - the 0.1.x shape, kept stable so existing
|
|
253
|
+
* consumers' save formats are untouched. A product embedding the
|
|
254
|
+
* versioned cross-product shape uses `saveFragment`. */
|
|
126
255
|
save() {
|
|
127
256
|
const out = {};
|
|
128
|
-
for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] =
|
|
257
|
+
for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = e.bag.save();
|
|
129
258
|
return out;
|
|
130
259
|
}
|
|
131
|
-
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
260
|
+
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
261
|
+
* are ignored. */
|
|
132
262
|
load(blob) {
|
|
133
263
|
for (const [token, vals] of Object.entries(blob)) {
|
|
134
264
|
const e = this.scopes.get(token);
|
|
135
|
-
if (e?.kind === "owned")
|
|
265
|
+
if (e?.kind === "owned") e.bag.load(vals);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** The versioned owned-state fragment (the one serialisation shape both
|
|
269
|
+
* product families' save envelopes embed when they adopt the kernel;
|
|
270
|
+
* design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
|
|
271
|
+
saveFragment() {
|
|
272
|
+
return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };
|
|
273
|
+
}
|
|
274
|
+
/** Restore from a versioned fragment; an unsupported version throws. */
|
|
275
|
+
loadFragment(fragment) {
|
|
276
|
+
if (fragment.version !== SAVE_FRAGMENT_VERSION) {
|
|
277
|
+
throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);
|
|
136
278
|
}
|
|
279
|
+
this.load(fragment.scopes);
|
|
137
280
|
}
|
|
138
281
|
assertFree(token) {
|
|
139
282
|
if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);
|
|
@@ -155,6 +298,8 @@ function defaultFor(d) {
|
|
|
155
298
|
}
|
|
156
299
|
}
|
|
157
300
|
|
|
301
|
+
exports.PropertyBag = PropertyBag;
|
|
302
|
+
exports.SAVE_FRAGMENT_VERSION = SAVE_FRAGMENT_VERSION;
|
|
158
303
|
exports.SUPPORTED_SPEC_VERSIONS = SUPPORTED_SPEC_VERSIONS;
|
|
159
304
|
exports.ScopeRegistry = ScopeRegistry;
|
|
160
305
|
exports.readScopeRegistrySpec = readScopeRegistrySpec;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: Record<string, ScalarValue>;\n decls: Map<string, ScopeDeclaration>;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.assertFree(token);\n const bag: Record<string, ScalarValue> = {};\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n decls.set(name, d);\n bag[name] = d.default ?? defaultFor(d);\n }\n this.scopes.set(token, { kind: \"owned\", bag, decls });\n return this;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n for (const k of Object.keys(e.bag)) delete e.bag[k];\n e.decls.clear();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n e.decls.set(name, d);\n e.bag[name] = d.default ?? defaultFor(d);\n }\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n const n = name.toLowerCase();\n return e.kind === \"owned\" ? e.bag[n] : e.resolver.get(n);\n }\n\n /** Write a property. Throws on an unknown or read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n const n = name.toLowerCase();\n if (!this.writable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n if (e.kind === \"owned\") e.bag[n] = value;\n else e.resolver.set!(n, value);\n }\n\n private writable(e: Entry, name: string): boolean {\n if (e.kind === \"owned\") return e.decls.get(name)?.writable ?? true;\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag : e.resolver;\n }\n return { scopes, host };\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[] }>>();\n for (const [token, e] of this.scopes) {\n if (e.decls.size === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[] }>();\n for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = { ...e.bag };\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") Object.assign(e.bag, vals);\n }\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n constructor(declarations: ScopeDeclaration[] = [], opts?: { normalise?: (name: string) => string }) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(d: ScopeDeclaration, value: ScalarValue | undefined, writable?: boolean, name?: string): PropertyRow {\n return {\n name: name ?? d.name.toLowerCase(),\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n return this.mountOwned(token, new PropertyBag(declarations));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase())),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n return { scopes, host };\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), { type: d.type, enumValues: d.values });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ScalarValue, PropertyType, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
|
|
2
2
|
export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -39,6 +39,79 @@ declare const SUPPORTED_SPEC_VERSIONS: readonly [1];
|
|
|
39
39
|
* throws on a malformed or unsupported-version spec.
|
|
40
40
|
*/
|
|
41
41
|
declare function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null;
|
|
42
|
+
/** One property change. `silent` marks a host write (the firing rule: it
|
|
43
|
+
* reaches the audit hook but not subscribers); `reason` is the host's own
|
|
44
|
+
* note for its log. */
|
|
45
|
+
interface BagChange {
|
|
46
|
+
name: string;
|
|
47
|
+
prev?: ScalarValue;
|
|
48
|
+
next: ScalarValue;
|
|
49
|
+
silent: boolean;
|
|
50
|
+
reason?: string;
|
|
51
|
+
}
|
|
52
|
+
/** One examiner row: what a property examiner/editor needs to render and
|
|
53
|
+
* edit a declared property. */
|
|
54
|
+
interface PropertyRow {
|
|
55
|
+
name: string;
|
|
56
|
+
type: PropertyType;
|
|
57
|
+
value: ScalarValue | undefined;
|
|
58
|
+
default: ScalarValue;
|
|
59
|
+
values?: string[];
|
|
60
|
+
writable: boolean;
|
|
61
|
+
}
|
|
62
|
+
declare class PropertyBag {
|
|
63
|
+
/** The live values record (stable identity across reseed, so an
|
|
64
|
+
* EvalContext built over it stays valid). Read-path for evaluation;
|
|
65
|
+
* writes go through `set` so the firing rule applies. */
|
|
66
|
+
readonly values: Record<string, ScalarValue>;
|
|
67
|
+
private decls;
|
|
68
|
+
private readonly subscribers;
|
|
69
|
+
private readonly auditors;
|
|
70
|
+
/** Name normalisation policy: lowercase by default (the registry's
|
|
71
|
+
* long-standing contract); a product whose names are case-significant
|
|
72
|
+
* passes identity. */
|
|
73
|
+
private readonly norm;
|
|
74
|
+
constructor(declarations?: ScopeDeclaration[], opts?: {
|
|
75
|
+
normalise?: (name: string) => string;
|
|
76
|
+
});
|
|
77
|
+
private seed;
|
|
78
|
+
get(name: string): ScalarValue | undefined;
|
|
79
|
+
/** Write a property. Engine writes (the default) notify subscribers;
|
|
80
|
+
* pass `silent: true` for a host write, which reaches only the audit
|
|
81
|
+
* hook. Throws on a read-only property. Returns the change. */
|
|
82
|
+
set(name: string, value: ScalarValue, opts?: {
|
|
83
|
+
silent?: boolean;
|
|
84
|
+
reason?: string;
|
|
85
|
+
}): BagChange;
|
|
86
|
+
/** Notified of engine (non-silent) writes. Returns the unsubscribe. */
|
|
87
|
+
subscribe(fn: (change: BagChange) => void): () => void;
|
|
88
|
+
/** Notified of EVERY write, silent or not. Returns the unsubscribe. */
|
|
89
|
+
onAudit(fn: (change: BagChange) => void): () => void;
|
|
90
|
+
/** Examiner rows: the declared surface only (stray values are storage,
|
|
91
|
+
* not surface). */
|
|
92
|
+
rows(): PropertyRow[];
|
|
93
|
+
declarations(): ScopeDeclaration[];
|
|
94
|
+
/** The one sanctioned copy door: values deep-copied, declarations
|
|
95
|
+
* duplicated, the normalisation policy carried, subscriptions NOT
|
|
96
|
+
* carried. */
|
|
97
|
+
clone(): PropertyBag;
|
|
98
|
+
/** Clear and re-seed from new declarations, in place (the values record
|
|
99
|
+
* keeps its identity, so contexts built over it stay valid). */
|
|
100
|
+
reseed(declarations: ScopeDeclaration[]): void;
|
|
101
|
+
/** Bare values, ready to embed in a product's save. */
|
|
102
|
+
save(): Record<string, ScalarValue>;
|
|
103
|
+
/** Lay saved values over the current ones (call after a fresh seed:
|
|
104
|
+
* orphans land as strays, new declarations keep their defaults; the
|
|
105
|
+
* product decides whether to prune). Does not fire events. */
|
|
106
|
+
load(values: Record<string, ScalarValue>): void;
|
|
107
|
+
}
|
|
108
|
+
/** The versioned owned-state fragment both product save envelopes embed
|
|
109
|
+
* (design/engine-runtimes.md 3.1: one serialisation shape for bags). */
|
|
110
|
+
interface OwnedStateFragment {
|
|
111
|
+
version: number;
|
|
112
|
+
scopes: Record<string, Record<string, ScalarValue>>;
|
|
113
|
+
}
|
|
114
|
+
declare const SAVE_FRAGMENT_VERSION = 1;
|
|
42
115
|
declare class ScopeRegistry {
|
|
43
116
|
private readonly scopes;
|
|
44
117
|
/**
|
|
@@ -47,6 +120,14 @@ declare class ScopeRegistry {
|
|
|
47
120
|
* type-checked (declarations) and serialized by `save`/`load`.
|
|
48
121
|
*/
|
|
49
122
|
defineOwned(token: string, declarations: ScopeDeclaration[]): this;
|
|
123
|
+
/**
|
|
124
|
+
* Attach an EXISTING bag as an owned scope - the shared-container move: a
|
|
125
|
+
* host (or the other product) holds the bag; this registry reads, writes
|
|
126
|
+
* and lists it like its own, but the holder saves it.
|
|
127
|
+
*/
|
|
128
|
+
mountOwned(token: string, bag: PropertyBag): this;
|
|
129
|
+
/** An owned scope's bag (subscribe, audit, rows live there). */
|
|
130
|
+
ownedBag(token: string): PropertyBag;
|
|
50
131
|
/**
|
|
51
132
|
* Re-initialise an existing **owned** scope's bag from new declarations,
|
|
52
133
|
* clearing its current values. For scope-local state that resets on a context
|
|
@@ -65,9 +146,17 @@ declare class ScopeRegistry {
|
|
|
65
146
|
has(token: string): boolean;
|
|
66
147
|
/** Read a property; undefined if the scope or property is not present. */
|
|
67
148
|
get(scope: string, name: string): ScalarValue | undefined;
|
|
68
|
-
/** Write a property
|
|
149
|
+
/** Write a property (an ENGINE write: the bag's subscribers fire; use
|
|
150
|
+
* the bag directly for silent host writes). Throws on an unknown or
|
|
151
|
+
* read-only scope/property. */
|
|
69
152
|
set(scope: string, name: string, value: ScalarValue): void;
|
|
70
|
-
private
|
|
153
|
+
private foreignWritable;
|
|
154
|
+
/** Examiner rows across every scope with a declared surface: owned bags
|
|
155
|
+
* first, then declared foreign scopes (values read through, writability
|
|
156
|
+
* reflecting the resolver). Opaque foreign scopes are not listed. */
|
|
157
|
+
listProperties(): ({
|
|
158
|
+
scope: string;
|
|
159
|
+
} & PropertyRow)[];
|
|
71
160
|
/**
|
|
72
161
|
* Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
|
|
73
162
|
* bags, foreign scopes as their resolvers. `host` carries dialect-function
|
|
@@ -80,11 +169,21 @@ declare class ScopeRegistry {
|
|
|
80
169
|
* declared scopes contribute their property types for validation.
|
|
81
170
|
*/
|
|
82
171
|
toSchema(): ExpressionSchema;
|
|
83
|
-
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
172
|
+
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
173
|
+
* host-saved), as bare bags - the 0.1.x shape, kept stable so existing
|
|
174
|
+
* consumers' save formats are untouched. A product embedding the
|
|
175
|
+
* versioned cross-product shape uses `saveFragment`. */
|
|
84
176
|
save(): Record<string, Record<string, ScalarValue>>;
|
|
85
|
-
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
177
|
+
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
178
|
+
* are ignored. */
|
|
86
179
|
load(blob: Record<string, Record<string, ScalarValue>>): void;
|
|
180
|
+
/** The versioned owned-state fragment (the one serialisation shape both
|
|
181
|
+
* product families' save envelopes embed when they adopt the kernel;
|
|
182
|
+
* design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
|
|
183
|
+
saveFragment(): OwnedStateFragment;
|
|
184
|
+
/** Restore from a versioned fragment; an unsupported version throws. */
|
|
185
|
+
loadFragment(fragment: OwnedStateFragment): void;
|
|
87
186
|
private assertFree;
|
|
88
187
|
}
|
|
89
188
|
|
|
90
|
-
export { SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
|
|
189
|
+
export { type BagChange, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ScalarValue, PropertyType, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
|
|
2
2
|
export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -39,6 +39,79 @@ declare const SUPPORTED_SPEC_VERSIONS: readonly [1];
|
|
|
39
39
|
* throws on a malformed or unsupported-version spec.
|
|
40
40
|
*/
|
|
41
41
|
declare function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null;
|
|
42
|
+
/** One property change. `silent` marks a host write (the firing rule: it
|
|
43
|
+
* reaches the audit hook but not subscribers); `reason` is the host's own
|
|
44
|
+
* note for its log. */
|
|
45
|
+
interface BagChange {
|
|
46
|
+
name: string;
|
|
47
|
+
prev?: ScalarValue;
|
|
48
|
+
next: ScalarValue;
|
|
49
|
+
silent: boolean;
|
|
50
|
+
reason?: string;
|
|
51
|
+
}
|
|
52
|
+
/** One examiner row: what a property examiner/editor needs to render and
|
|
53
|
+
* edit a declared property. */
|
|
54
|
+
interface PropertyRow {
|
|
55
|
+
name: string;
|
|
56
|
+
type: PropertyType;
|
|
57
|
+
value: ScalarValue | undefined;
|
|
58
|
+
default: ScalarValue;
|
|
59
|
+
values?: string[];
|
|
60
|
+
writable: boolean;
|
|
61
|
+
}
|
|
62
|
+
declare class PropertyBag {
|
|
63
|
+
/** The live values record (stable identity across reseed, so an
|
|
64
|
+
* EvalContext built over it stays valid). Read-path for evaluation;
|
|
65
|
+
* writes go through `set` so the firing rule applies. */
|
|
66
|
+
readonly values: Record<string, ScalarValue>;
|
|
67
|
+
private decls;
|
|
68
|
+
private readonly subscribers;
|
|
69
|
+
private readonly auditors;
|
|
70
|
+
/** Name normalisation policy: lowercase by default (the registry's
|
|
71
|
+
* long-standing contract); a product whose names are case-significant
|
|
72
|
+
* passes identity. */
|
|
73
|
+
private readonly norm;
|
|
74
|
+
constructor(declarations?: ScopeDeclaration[], opts?: {
|
|
75
|
+
normalise?: (name: string) => string;
|
|
76
|
+
});
|
|
77
|
+
private seed;
|
|
78
|
+
get(name: string): ScalarValue | undefined;
|
|
79
|
+
/** Write a property. Engine writes (the default) notify subscribers;
|
|
80
|
+
* pass `silent: true` for a host write, which reaches only the audit
|
|
81
|
+
* hook. Throws on a read-only property. Returns the change. */
|
|
82
|
+
set(name: string, value: ScalarValue, opts?: {
|
|
83
|
+
silent?: boolean;
|
|
84
|
+
reason?: string;
|
|
85
|
+
}): BagChange;
|
|
86
|
+
/** Notified of engine (non-silent) writes. Returns the unsubscribe. */
|
|
87
|
+
subscribe(fn: (change: BagChange) => void): () => void;
|
|
88
|
+
/** Notified of EVERY write, silent or not. Returns the unsubscribe. */
|
|
89
|
+
onAudit(fn: (change: BagChange) => void): () => void;
|
|
90
|
+
/** Examiner rows: the declared surface only (stray values are storage,
|
|
91
|
+
* not surface). */
|
|
92
|
+
rows(): PropertyRow[];
|
|
93
|
+
declarations(): ScopeDeclaration[];
|
|
94
|
+
/** The one sanctioned copy door: values deep-copied, declarations
|
|
95
|
+
* duplicated, the normalisation policy carried, subscriptions NOT
|
|
96
|
+
* carried. */
|
|
97
|
+
clone(): PropertyBag;
|
|
98
|
+
/** Clear and re-seed from new declarations, in place (the values record
|
|
99
|
+
* keeps its identity, so contexts built over it stay valid). */
|
|
100
|
+
reseed(declarations: ScopeDeclaration[]): void;
|
|
101
|
+
/** Bare values, ready to embed in a product's save. */
|
|
102
|
+
save(): Record<string, ScalarValue>;
|
|
103
|
+
/** Lay saved values over the current ones (call after a fresh seed:
|
|
104
|
+
* orphans land as strays, new declarations keep their defaults; the
|
|
105
|
+
* product decides whether to prune). Does not fire events. */
|
|
106
|
+
load(values: Record<string, ScalarValue>): void;
|
|
107
|
+
}
|
|
108
|
+
/** The versioned owned-state fragment both product save envelopes embed
|
|
109
|
+
* (design/engine-runtimes.md 3.1: one serialisation shape for bags). */
|
|
110
|
+
interface OwnedStateFragment {
|
|
111
|
+
version: number;
|
|
112
|
+
scopes: Record<string, Record<string, ScalarValue>>;
|
|
113
|
+
}
|
|
114
|
+
declare const SAVE_FRAGMENT_VERSION = 1;
|
|
42
115
|
declare class ScopeRegistry {
|
|
43
116
|
private readonly scopes;
|
|
44
117
|
/**
|
|
@@ -47,6 +120,14 @@ declare class ScopeRegistry {
|
|
|
47
120
|
* type-checked (declarations) and serialized by `save`/`load`.
|
|
48
121
|
*/
|
|
49
122
|
defineOwned(token: string, declarations: ScopeDeclaration[]): this;
|
|
123
|
+
/**
|
|
124
|
+
* Attach an EXISTING bag as an owned scope - the shared-container move: a
|
|
125
|
+
* host (or the other product) holds the bag; this registry reads, writes
|
|
126
|
+
* and lists it like its own, but the holder saves it.
|
|
127
|
+
*/
|
|
128
|
+
mountOwned(token: string, bag: PropertyBag): this;
|
|
129
|
+
/** An owned scope's bag (subscribe, audit, rows live there). */
|
|
130
|
+
ownedBag(token: string): PropertyBag;
|
|
50
131
|
/**
|
|
51
132
|
* Re-initialise an existing **owned** scope's bag from new declarations,
|
|
52
133
|
* clearing its current values. For scope-local state that resets on a context
|
|
@@ -65,9 +146,17 @@ declare class ScopeRegistry {
|
|
|
65
146
|
has(token: string): boolean;
|
|
66
147
|
/** Read a property; undefined if the scope or property is not present. */
|
|
67
148
|
get(scope: string, name: string): ScalarValue | undefined;
|
|
68
|
-
/** Write a property
|
|
149
|
+
/** Write a property (an ENGINE write: the bag's subscribers fire; use
|
|
150
|
+
* the bag directly for silent host writes). Throws on an unknown or
|
|
151
|
+
* read-only scope/property. */
|
|
69
152
|
set(scope: string, name: string, value: ScalarValue): void;
|
|
70
|
-
private
|
|
153
|
+
private foreignWritable;
|
|
154
|
+
/** Examiner rows across every scope with a declared surface: owned bags
|
|
155
|
+
* first, then declared foreign scopes (values read through, writability
|
|
156
|
+
* reflecting the resolver). Opaque foreign scopes are not listed. */
|
|
157
|
+
listProperties(): ({
|
|
158
|
+
scope: string;
|
|
159
|
+
} & PropertyRow)[];
|
|
71
160
|
/**
|
|
72
161
|
* Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
|
|
73
162
|
* bags, foreign scopes as their resolvers. `host` carries dialect-function
|
|
@@ -80,11 +169,21 @@ declare class ScopeRegistry {
|
|
|
80
169
|
* declared scopes contribute their property types for validation.
|
|
81
170
|
*/
|
|
82
171
|
toSchema(): ExpressionSchema;
|
|
83
|
-
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
172
|
+
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
173
|
+
* host-saved), as bare bags - the 0.1.x shape, kept stable so existing
|
|
174
|
+
* consumers' save formats are untouched. A product embedding the
|
|
175
|
+
* versioned cross-product shape uses `saveFragment`. */
|
|
84
176
|
save(): Record<string, Record<string, ScalarValue>>;
|
|
85
|
-
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
177
|
+
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
178
|
+
* are ignored. */
|
|
86
179
|
load(blob: Record<string, Record<string, ScalarValue>>): void;
|
|
180
|
+
/** The versioned owned-state fragment (the one serialisation shape both
|
|
181
|
+
* product families' save envelopes embed when they adopt the kernel;
|
|
182
|
+
* design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
|
|
183
|
+
saveFragment(): OwnedStateFragment;
|
|
184
|
+
/** Restore from a versioned fragment; an unsupported version throws. */
|
|
185
|
+
loadFragment(fragment: OwnedStateFragment): void;
|
|
87
186
|
private assertFree;
|
|
88
187
|
}
|
|
89
188
|
|
|
90
|
-
export { SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
|
|
189
|
+
export { type BagChange, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,106 @@ function readScopeRegistrySpec(source) {
|
|
|
18
18
|
}
|
|
19
19
|
return spec;
|
|
20
20
|
}
|
|
21
|
+
var PropertyBag = class _PropertyBag {
|
|
22
|
+
/** The live values record (stable identity across reseed, so an
|
|
23
|
+
* EvalContext built over it stays valid). Read-path for evaluation;
|
|
24
|
+
* writes go through `set` so the firing rule applies. */
|
|
25
|
+
values = {};
|
|
26
|
+
decls = /* @__PURE__ */ new Map();
|
|
27
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
28
|
+
auditors = /* @__PURE__ */ new Set();
|
|
29
|
+
/** Name normalisation policy: lowercase by default (the registry's
|
|
30
|
+
* long-standing contract); a product whose names are case-significant
|
|
31
|
+
* passes identity. */
|
|
32
|
+
norm;
|
|
33
|
+
constructor(declarations = [], opts) {
|
|
34
|
+
this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
|
|
35
|
+
this.seed(declarations);
|
|
36
|
+
}
|
|
37
|
+
seed(declarations) {
|
|
38
|
+
for (const d of declarations) {
|
|
39
|
+
const name = this.norm(d.name);
|
|
40
|
+
this.decls.set(name, d);
|
|
41
|
+
this.values[name] = structuredClone(d.default ?? defaultFor(d));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
get(name) {
|
|
45
|
+
return this.values[this.norm(name)];
|
|
46
|
+
}
|
|
47
|
+
/** Write a property. Engine writes (the default) notify subscribers;
|
|
48
|
+
* pass `silent: true` for a host write, which reaches only the audit
|
|
49
|
+
* hook. Throws on a read-only property. Returns the change. */
|
|
50
|
+
set(name, value, opts) {
|
|
51
|
+
const n = this.norm(name);
|
|
52
|
+
if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
53
|
+
const change = {
|
|
54
|
+
name: n,
|
|
55
|
+
prev: this.values[n],
|
|
56
|
+
next: value,
|
|
57
|
+
silent: opts?.silent ?? false,
|
|
58
|
+
reason: opts?.reason
|
|
59
|
+
};
|
|
60
|
+
this.values[n] = value;
|
|
61
|
+
for (const audit of this.auditors) audit(change);
|
|
62
|
+
if (!change.silent) for (const fn of this.subscribers) fn(change);
|
|
63
|
+
return change;
|
|
64
|
+
}
|
|
65
|
+
/** Notified of engine (non-silent) writes. Returns the unsubscribe. */
|
|
66
|
+
subscribe(fn) {
|
|
67
|
+
this.subscribers.add(fn);
|
|
68
|
+
return () => this.subscribers.delete(fn);
|
|
69
|
+
}
|
|
70
|
+
/** Notified of EVERY write, silent or not. Returns the unsubscribe. */
|
|
71
|
+
onAudit(fn) {
|
|
72
|
+
this.auditors.add(fn);
|
|
73
|
+
return () => this.auditors.delete(fn);
|
|
74
|
+
}
|
|
75
|
+
/** Examiner rows: the declared surface only (stray values are storage,
|
|
76
|
+
* not surface). */
|
|
77
|
+
rows() {
|
|
78
|
+
return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name));
|
|
79
|
+
}
|
|
80
|
+
declarations() {
|
|
81
|
+
return [...this.decls.values()];
|
|
82
|
+
}
|
|
83
|
+
/** The one sanctioned copy door: values deep-copied, declarations
|
|
84
|
+
* duplicated, the normalisation policy carried, subscriptions NOT
|
|
85
|
+
* carried. */
|
|
86
|
+
clone() {
|
|
87
|
+
const c = new _PropertyBag([], { normalise: this.norm });
|
|
88
|
+
c.decls = new Map(this.decls);
|
|
89
|
+
Object.assign(c.values, structuredClone(this.values));
|
|
90
|
+
return c;
|
|
91
|
+
}
|
|
92
|
+
/** Clear and re-seed from new declarations, in place (the values record
|
|
93
|
+
* keeps its identity, so contexts built over it stay valid). */
|
|
94
|
+
reseed(declarations) {
|
|
95
|
+
for (const k of Object.keys(this.values)) delete this.values[k];
|
|
96
|
+
this.decls.clear();
|
|
97
|
+
this.seed(declarations);
|
|
98
|
+
}
|
|
99
|
+
/** Bare values, ready to embed in a product's save. */
|
|
100
|
+
save() {
|
|
101
|
+
return structuredClone(this.values);
|
|
102
|
+
}
|
|
103
|
+
/** Lay saved values over the current ones (call after a fresh seed:
|
|
104
|
+
* orphans land as strays, new declarations keep their defaults; the
|
|
105
|
+
* product decides whether to prune). Does not fire events. */
|
|
106
|
+
load(values) {
|
|
107
|
+
for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
function rowFor(d, value, writable, name) {
|
|
111
|
+
return {
|
|
112
|
+
name: name ?? d.name.toLowerCase(),
|
|
113
|
+
type: d.type,
|
|
114
|
+
value,
|
|
115
|
+
default: d.default ?? defaultFor(d),
|
|
116
|
+
...d.values !== void 0 ? { values: d.values } : {},
|
|
117
|
+
writable: writable ?? d.writable ?? true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var SAVE_FRAGMENT_VERSION = 1;
|
|
21
121
|
var ScopeRegistry = class {
|
|
22
122
|
scopes = /* @__PURE__ */ new Map();
|
|
23
123
|
/**
|
|
@@ -26,17 +126,24 @@ var ScopeRegistry = class {
|
|
|
26
126
|
* type-checked (declarations) and serialized by `save`/`load`.
|
|
27
127
|
*/
|
|
28
128
|
defineOwned(token, declarations) {
|
|
129
|
+
return this.mountOwned(token, new PropertyBag(declarations));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Attach an EXISTING bag as an owned scope - the shared-container move: a
|
|
133
|
+
* host (or the other product) holds the bag; this registry reads, writes
|
|
134
|
+
* and lists it like its own, but the holder saves it.
|
|
135
|
+
*/
|
|
136
|
+
mountOwned(token, bag) {
|
|
29
137
|
this.assertFree(token);
|
|
30
|
-
|
|
31
|
-
const decls = /* @__PURE__ */ new Map();
|
|
32
|
-
for (const d of declarations) {
|
|
33
|
-
const name = d.name.toLowerCase();
|
|
34
|
-
decls.set(name, d);
|
|
35
|
-
bag[name] = d.default ?? defaultFor(d);
|
|
36
|
-
}
|
|
37
|
-
this.scopes.set(token, { kind: "owned", bag, decls });
|
|
138
|
+
this.scopes.set(token, { kind: "owned", bag });
|
|
38
139
|
return this;
|
|
39
140
|
}
|
|
141
|
+
/** An owned scope's bag (subscribe, audit, rows live there). */
|
|
142
|
+
ownedBag(token) {
|
|
143
|
+
const e = this.scopes.get(token);
|
|
144
|
+
if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
|
|
145
|
+
return e.bag;
|
|
146
|
+
}
|
|
40
147
|
/**
|
|
41
148
|
* Re-initialise an existing **owned** scope's bag from new declarations,
|
|
42
149
|
* clearing its current values. For scope-local state that resets on a context
|
|
@@ -45,15 +152,7 @@ var ScopeRegistry = class {
|
|
|
45
152
|
* registry stays valid.
|
|
46
153
|
*/
|
|
47
154
|
reseedOwned(token, declarations) {
|
|
48
|
-
|
|
49
|
-
if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
|
|
50
|
-
for (const k of Object.keys(e.bag)) delete e.bag[k];
|
|
51
|
-
e.decls.clear();
|
|
52
|
-
for (const d of declarations) {
|
|
53
|
-
const name = d.name.toLowerCase();
|
|
54
|
-
e.decls.set(name, d);
|
|
55
|
-
e.bag[name] = d.default ?? defaultFor(d);
|
|
56
|
-
}
|
|
155
|
+
this.ownedBag(token).reseed(declarations);
|
|
57
156
|
return this;
|
|
58
157
|
}
|
|
59
158
|
/**
|
|
@@ -76,23 +175,49 @@ var ScopeRegistry = class {
|
|
|
76
175
|
get(scope, name) {
|
|
77
176
|
const e = this.scopes.get(scope);
|
|
78
177
|
if (!e) return void 0;
|
|
79
|
-
|
|
80
|
-
return e.kind === "owned" ? e.bag[n] : e.resolver.get(n);
|
|
178
|
+
return e.kind === "owned" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());
|
|
81
179
|
}
|
|
82
|
-
/** Write a property
|
|
180
|
+
/** Write a property (an ENGINE write: the bag's subscribers fire; use
|
|
181
|
+
* the bag directly for silent host writes). Throws on an unknown or
|
|
182
|
+
* read-only scope/property. */
|
|
83
183
|
set(scope, name, value) {
|
|
84
184
|
const e = this.scopes.get(scope);
|
|
85
185
|
if (!e) throw new Error(`unknown scope '@${scope}'`);
|
|
186
|
+
if (e.kind === "owned") {
|
|
187
|
+
try {
|
|
188
|
+
e.bag.set(name, value);
|
|
189
|
+
} catch {
|
|
190
|
+
throw new Error(`'@${scope}.${name}' is read-only`);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
86
194
|
const n = name.toLowerCase();
|
|
87
|
-
if (!this.
|
|
88
|
-
|
|
89
|
-
else e.resolver.set(n, value);
|
|
195
|
+
if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);
|
|
196
|
+
e.resolver.set(n, value);
|
|
90
197
|
}
|
|
91
|
-
|
|
92
|
-
if (e.kind === "owned") return e.decls.get(name)?.writable ?? true;
|
|
198
|
+
foreignWritable(e, name) {
|
|
93
199
|
if (!e.resolver.set) return false;
|
|
94
200
|
return e.decls.get(name)?.writable ?? e.scopeWritable;
|
|
95
201
|
}
|
|
202
|
+
/** Examiner rows across every scope with a declared surface: owned bags
|
|
203
|
+
* first, then declared foreign scopes (values read through, writability
|
|
204
|
+
* reflecting the resolver). Opaque foreign scopes are not listed. */
|
|
205
|
+
listProperties() {
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const [token, e] of this.scopes) {
|
|
208
|
+
if (e.kind === "owned") {
|
|
209
|
+
for (const row of e.bag.rows()) out.push({ scope: token, ...row });
|
|
210
|
+
} else {
|
|
211
|
+
for (const d of e.decls.values()) {
|
|
212
|
+
out.push({
|
|
213
|
+
scope: token,
|
|
214
|
+
...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()))
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
96
221
|
/**
|
|
97
222
|
* Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
|
|
98
223
|
* bags, foreign scopes as their resolvers. `host` carries dialect-function
|
|
@@ -101,7 +226,7 @@ var ScopeRegistry = class {
|
|
|
101
226
|
toEvalContext(host) {
|
|
102
227
|
const scopes = {};
|
|
103
228
|
for (const [token, e] of this.scopes) {
|
|
104
|
-
scopes[token] = e.kind === "owned" ? e.bag : e.resolver;
|
|
229
|
+
scopes[token] = e.kind === "owned" ? e.bag.values : e.resolver;
|
|
105
230
|
}
|
|
106
231
|
return { scopes, host };
|
|
107
232
|
}
|
|
@@ -113,25 +238,43 @@ var ScopeRegistry = class {
|
|
|
113
238
|
toSchema() {
|
|
114
239
|
const properties = /* @__PURE__ */ new Map();
|
|
115
240
|
for (const [token, e] of this.scopes) {
|
|
116
|
-
|
|
241
|
+
const decls = e.kind === "owned" ? e.bag.declarations() : [...e.decls.values()];
|
|
242
|
+
if (decls.length === 0) continue;
|
|
117
243
|
const m = /* @__PURE__ */ new Map();
|
|
118
|
-
for (const
|
|
244
|
+
for (const d of decls) m.set(d.name.toLowerCase(), { type: d.type, enumValues: d.values });
|
|
119
245
|
properties.set(token, m);
|
|
120
246
|
}
|
|
121
247
|
return { properties };
|
|
122
248
|
}
|
|
123
|
-
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
249
|
+
/** Serialize **owned** scopes only (foreign scopes are host-owned,
|
|
250
|
+
* host-saved), as bare bags - the 0.1.x shape, kept stable so existing
|
|
251
|
+
* consumers' save formats are untouched. A product embedding the
|
|
252
|
+
* versioned cross-product shape uses `saveFragment`. */
|
|
124
253
|
save() {
|
|
125
254
|
const out = {};
|
|
126
|
-
for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] =
|
|
255
|
+
for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = e.bag.save();
|
|
127
256
|
return out;
|
|
128
257
|
}
|
|
129
|
-
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
258
|
+
/** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
|
|
259
|
+
* are ignored. */
|
|
130
260
|
load(blob) {
|
|
131
261
|
for (const [token, vals] of Object.entries(blob)) {
|
|
132
262
|
const e = this.scopes.get(token);
|
|
133
|
-
if (e?.kind === "owned")
|
|
263
|
+
if (e?.kind === "owned") e.bag.load(vals);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** The versioned owned-state fragment (the one serialisation shape both
|
|
267
|
+
* product families' save envelopes embed when they adopt the kernel;
|
|
268
|
+
* design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
|
|
269
|
+
saveFragment() {
|
|
270
|
+
return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };
|
|
271
|
+
}
|
|
272
|
+
/** Restore from a versioned fragment; an unsupported version throws. */
|
|
273
|
+
loadFragment(fragment) {
|
|
274
|
+
if (fragment.version !== SAVE_FRAGMENT_VERSION) {
|
|
275
|
+
throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);
|
|
134
276
|
}
|
|
277
|
+
this.load(fragment.scopes);
|
|
135
278
|
}
|
|
136
279
|
assertFree(token) {
|
|
137
280
|
if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);
|
|
@@ -153,6 +296,6 @@ function defaultFor(d) {
|
|
|
153
296
|
}
|
|
154
297
|
}
|
|
155
298
|
|
|
156
|
-
export { SUPPORTED_SPEC_VERSIONS, ScopeRegistry, readScopeRegistrySpec };
|
|
299
|
+
export { PropertyBag, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, ScopeRegistry, readScopeRegistrySpec };
|
|
157
300
|
//# sourceMappingURL=index.js.map
|
|
158
301
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: Record<string, ScalarValue>;\n decls: Map<string, ScopeDeclaration>;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.assertFree(token);\n const bag: Record<string, ScalarValue> = {};\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n decls.set(name, d);\n bag[name] = d.default ?? defaultFor(d);\n }\n this.scopes.set(token, { kind: \"owned\", bag, decls });\n return this;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n for (const k of Object.keys(e.bag)) delete e.bag[k];\n e.decls.clear();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n e.decls.set(name, d);\n e.bag[name] = d.default ?? defaultFor(d);\n }\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n const n = name.toLowerCase();\n return e.kind === \"owned\" ? e.bag[n] : e.resolver.get(n);\n }\n\n /** Write a property. Throws on an unknown or read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n const n = name.toLowerCase();\n if (!this.writable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n if (e.kind === \"owned\") e.bag[n] = value;\n else e.resolver.set!(n, value);\n }\n\n private writable(e: Entry, name: string): boolean {\n if (e.kind === \"owned\") return e.decls.get(name)?.writable ?? true;\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag : e.resolver;\n }\n return { scopes, host };\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[] }>>();\n for (const [token, e] of this.scopes) {\n if (e.decls.size === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[] }>();\n for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = { ...e.bag };\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") Object.assign(e.bag, vals);\n }\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n constructor(declarations: ScopeDeclaration[] = [], opts?: { normalise?: (name: string) => string }) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(d: ScopeDeclaration, value: ScalarValue | undefined, writable?: boolean, name?: string): PropertyRow {\n return {\n name: name ?? d.name.toLowerCase(),\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n return this.mountOwned(token, new PropertyBag(declarations));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase())),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n return { scopes, host };\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), { type: d.type, enumValues: d.values });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wildwinter/scoperegistry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scope registry / runtime state container for @wildwinter/expr: owned property scopes + foreign (host-resolved) scopes, save/load, and the scopeRegistrySpec interop format. Produces the EvalContext and ExpressionSchema that expr consumes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|