@flighthq/registry 0.3.1-next.1041.35b950c
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/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/registryTable.d.ts +16 -0
- package/dist/registryTable.d.ts.map +1 -0
- package/dist/registryTable.js +162 -0
- package/dist/registryTable.js.map +1 -0
- package/package.json +48 -0
- package/src/registryTable.test.ts +272 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { concatRegistryTable, createKeyedTable, createOrdinalTable, createSlotTable, getOrdinalTableEntry, getRegistryTableEntry, getRegistryTableKeys, hasRegistryTableEntry, withoutRegistryTableEntry, withRegistryTableEntry, withRegistryTableTombstone, } from './registryTable';
|
|
2
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { concatRegistryTable, createKeyedTable, createOrdinalTable, createSlotTable, getOrdinalTableEntry, getRegistryTableEntry, getRegistryTableKeys, hasRegistryTableEntry, withoutRegistryTableEntry, withRegistryTableEntry, withRegistryTableTombstone, } from './registryTable';
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Kind, KeyedTable, OrdinalTable, RegistryId, RegistryMissPolicy, RegistryTable, SlotTable } from '@flighthq/types/contract';
|
|
2
|
+
export declare function concatRegistryTable<T>(base: Readonly<KeyedTable<T>> | Readonly<SlotTable<T>>, overlay: Readonly<KeyedTable<T>> | Readonly<SlotTable<T>>): KeyedTable<T> | SlotTable<T>;
|
|
3
|
+
/** An empty keyed table for `registry`, carrying the miss policy every replacement of it preserves. */
|
|
4
|
+
export declare function createKeyedTable<T>(registry: RegistryId, onMiss: RegistryMissPolicy): KeyedTable<T>;
|
|
5
|
+
/** An ordinal table over `vocabulary`, every ordinal unbound. */
|
|
6
|
+
export declare function createOrdinalTable<T>(registry: RegistryId, onMiss: RegistryMissPolicy, vocabulary: readonly Kind[]): OrdinalTable<T>;
|
|
7
|
+
/** An empty slot table for `registry`. Its key is its own `RegistryId`. */
|
|
8
|
+
export declare function createSlotTable<T>(registry: RegistryId, onMiss: RegistryMissPolicy): SlotTable<T>;
|
|
9
|
+
export declare function getOrdinalTableEntry<T>(table: Readonly<OrdinalTable<T>>, ordinal: number): T | null;
|
|
10
|
+
export declare function getRegistryTableEntry<T>(table: Readonly<RegistryTable<T>>, key: Kind): T | null;
|
|
11
|
+
export declare function getRegistryTableKeys(out: Kind[], table: Readonly<RegistryTable<unknown>>): void;
|
|
12
|
+
export declare function hasRegistryTableEntry(table: Readonly<RegistryTable<unknown>>, key: Kind): boolean;
|
|
13
|
+
export declare function withoutRegistryTableEntry<T>(table: Readonly<KeyedTable<T>>, key: Kind): KeyedTable<T>;
|
|
14
|
+
export declare function withRegistryTableEntry<T>(table: Readonly<KeyedTable<T>>, key: Kind, value: T): KeyedTable<T>;
|
|
15
|
+
export declare function withRegistryTableTombstone<T>(table: Readonly<KeyedTable<T>>, key: Kind): KeyedTable<T>;
|
|
16
|
+
//# sourceMappingURL=registryTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryTable.d.ts","sourceRoot":"","sources":["../src/registryTable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,UAAU,EACV,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,aAAa,EAEb,SAAS,EACV,MAAM,0BAA0B,CAAC;AAsBlC,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EACtD,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GACxD,UAAU,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CA0C9B;AAED,uGAAuG;AACvG,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,kBAAkB,GAAG,UAAU,CAAC,CAAC,CAAC,CAEnG;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,QAAQ,EAAE,UAAU,EACpB,MAAM,EAAE,kBAAkB,EAC1B,UAAU,EAAE,SAAS,IAAI,EAAE,GAC1B,YAAY,CAAC,CAAC,CAAC,CAEjB;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAAC,CAAC,CAEjG;AAKD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAGnG;AAOD,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,GAAG,IAAI,CAI/F;AAKD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAc/F;AAID,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAGjG;AAQD,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,CAIrG;AAKD,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAI5G;AASD,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,CAItG"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { RegistryEntryState } from '@flighthq/types/contract';
|
|
2
|
+
// Registry tables are PERSISTENT: every operation that changes one returns a REPLACEMENT and mutates
|
|
3
|
+
// nothing, and the owner assigns the result. That is why the mutating verbs are named `with*` rather
|
|
4
|
+
// than `set*` — this repo reserves `set*` for in-place mutation, and a `set*` that does not set
|
|
5
|
+
// contradicts a stated rule at the call site, where the reader has only the name to go on.
|
|
6
|
+
//
|
|
7
|
+
// Every operation here is cold: construction, composition, enumeration, diagnostics. None is on a draw
|
|
8
|
+
// path. `getOrdinalTableEntry` is the one hot-path form and does a direct index with no dispatch.
|
|
9
|
+
// Composes `overlay` onto `base`, entry by entry. The overlay wins wherever it has an opinion.
|
|
10
|
+
//
|
|
11
|
+
// Refuses rather than guesses: composing two tables of different SHAPE, or belonging to different
|
|
12
|
+
// registries, is a programmer error and throws. It is not an expected failure a caller could handle.
|
|
13
|
+
//
|
|
14
|
+
// Three refusals, all the same kind of programmer error: a different SHAPE, a different REGISTRY, or a
|
|
15
|
+
// different MISS POLICY. The policy comparison is equality only — composition never interprets a policy,
|
|
16
|
+
// which is why the table layer needs no vocabulary of its own and the type is an open alias.
|
|
17
|
+
//
|
|
18
|
+
// `OrdinalTable` is not accepted, and that is structural rather than an oversight — nothing composes an
|
|
19
|
+
// ordinal table, so it carries no composition operations at all. The signature refuses it.
|
|
20
|
+
export function concatRegistryTable(base, overlay) {
|
|
21
|
+
if (base.shape !== overlay.shape) {
|
|
22
|
+
throw new Error(`concatRegistryTable: cannot compose a '${base.shape}' table with a '${overlay.shape}' table`);
|
|
23
|
+
}
|
|
24
|
+
if (base.registry !== overlay.registry) {
|
|
25
|
+
throw new Error(`concatRegistryTable: cannot compose registry '${base.registry}' with registry '${overlay.registry}'`);
|
|
26
|
+
}
|
|
27
|
+
if (base.onMiss !== overlay.onMiss) {
|
|
28
|
+
throw new Error(`concatRegistryTable: cannot compose miss policy '${base.onMiss}' with miss policy '${overlay.onMiss}'`);
|
|
29
|
+
}
|
|
30
|
+
if (base.shape === 'slot') {
|
|
31
|
+
const overlaySlot = overlay;
|
|
32
|
+
// `null` on the overlay is "no opinion" — inherit. A tombstone is "explicitly omitted" and wins.
|
|
33
|
+
return { entry: overlaySlot.entry ?? base.entry, onMiss: base.onMiss, registry: base.registry, shape: 'slot' };
|
|
34
|
+
}
|
|
35
|
+
const baseKeyed = base;
|
|
36
|
+
const overlayKeyed = overlay;
|
|
37
|
+
const entries = new Map(baseKeyed.entries);
|
|
38
|
+
for (const [key, entry] of overlayKeyed.entries) {
|
|
39
|
+
// The switch is what makes a THIRD state a build failure rather than a silent fall-through. Both
|
|
40
|
+
// arms carry the entry forward; a tombstone is copied AS a tombstone, never resolved to a binding,
|
|
41
|
+
// because resolving it here would resurrect the entry the overlay meant to omit.
|
|
42
|
+
switch (entry.state) {
|
|
43
|
+
case RegistryEntryState.Bound:
|
|
44
|
+
entries.set(key, entry);
|
|
45
|
+
break;
|
|
46
|
+
case RegistryEntryState.Tombstoned:
|
|
47
|
+
entries.set(key, entry);
|
|
48
|
+
break;
|
|
49
|
+
default: {
|
|
50
|
+
const unreachable = entry;
|
|
51
|
+
return unreachable;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { entries, onMiss: baseKeyed.onMiss, registry: baseKeyed.registry, shape: 'keyed' };
|
|
56
|
+
}
|
|
57
|
+
/** An empty keyed table for `registry`, carrying the miss policy every replacement of it preserves. */
|
|
58
|
+
export function createKeyedTable(registry, onMiss) {
|
|
59
|
+
return { entries: new Map(), onMiss, registry, shape: 'keyed' };
|
|
60
|
+
}
|
|
61
|
+
/** An ordinal table over `vocabulary`, every ordinal unbound. */
|
|
62
|
+
export function createOrdinalTable(registry, onMiss, vocabulary) {
|
|
63
|
+
return { entries: vocabulary.map(() => null), onMiss, registry, shape: 'ordinal', vocabulary };
|
|
64
|
+
}
|
|
65
|
+
/** An empty slot table for `registry`. Its key is its own `RegistryId`. */
|
|
66
|
+
export function createSlotTable(registry, onMiss) {
|
|
67
|
+
return { entry: null, onMiss, registry, shape: 'slot' };
|
|
68
|
+
}
|
|
69
|
+
// The hot-path ordinal form: a direct index, no hash and no string. Out-of-range returns `null`, which
|
|
70
|
+
// is the format's skip-what-you-do-not-know path rather than a miss — a caller distinguishing "unknown
|
|
71
|
+
// token" from "unregistered reader" compares the ordinal against `vocabulary.length`.
|
|
72
|
+
export function getOrdinalTableEntry(table, ordinal) {
|
|
73
|
+
if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal >= table.entries.length)
|
|
74
|
+
return null;
|
|
75
|
+
return table.entries[ordinal];
|
|
76
|
+
}
|
|
77
|
+
// What is bound to `key`, or `null`.
|
|
78
|
+
//
|
|
79
|
+
// RESOLUTION collapses a tombstone to `null`, and that is correct rather than lossy: the caller asked
|
|
80
|
+
// what is bound and the answer is nothing. No tombstone escapes into caller code through this door,
|
|
81
|
+
// which is why this returns `T | null` while composition deals in entries.
|
|
82
|
+
export function getRegistryTableEntry(table, key) {
|
|
83
|
+
const entry = getRegistryTableEntryState(table, key);
|
|
84
|
+
if (entry === null || entry.state !== RegistryEntryState.Bound)
|
|
85
|
+
return null;
|
|
86
|
+
return entry.value;
|
|
87
|
+
}
|
|
88
|
+
// Clears `out`, then appends every BOUND key in sorted order — a tombstoned key is NOT listed, and a
|
|
89
|
+
// caller that enumerates and then resolves therefore cannot get `null` for a key this just reported.
|
|
90
|
+
// Sorting here rather than in storage is what lets the keyed shape stay a map.
|
|
91
|
+
export function getRegistryTableKeys(out, table) {
|
|
92
|
+
out.length = 0;
|
|
93
|
+
if (table.shape === 'keyed') {
|
|
94
|
+
for (const [key, entry] of table.entries) {
|
|
95
|
+
if (entry.state === RegistryEntryState.Bound)
|
|
96
|
+
out.push(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (table.shape === 'slot') {
|
|
100
|
+
if (table.entry !== null && table.entry.state === RegistryEntryState.Bound)
|
|
101
|
+
out.push(table.registry);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
for (let ordinal = 0; ordinal < table.entries.length; ordinal++) {
|
|
105
|
+
if (table.entries[ordinal] !== null)
|
|
106
|
+
out.push(table.vocabulary[ordinal]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
out.sort();
|
|
110
|
+
}
|
|
111
|
+
// FALSE for a tombstoned key — the same rule as `getRegistryTableKeys`, one call apart: `has` answering
|
|
112
|
+
// true where `get` answers null is that disagreement wearing a different name.
|
|
113
|
+
export function hasRegistryTableEntry(table, key) {
|
|
114
|
+
const entry = getRegistryTableEntryState(table, key);
|
|
115
|
+
return entry !== null && entry.state === RegistryEntryState.Bound;
|
|
116
|
+
}
|
|
117
|
+
// NO OPINION. Removes `key` from the table entirely, so under composition the base's entry is INHERITED.
|
|
118
|
+
//
|
|
119
|
+
// The opposite of `withRegistryTableTombstone`: that one overrides the base with nothing, this one
|
|
120
|
+
// declines to override at all. There is no third union variant for this and there should not be — "no
|
|
121
|
+
// opinion" IS the key being absent from the map, so there is nothing to store and therefore nothing to
|
|
122
|
+
// type. Typing it would be the tombstone.
|
|
123
|
+
export function withoutRegistryTableEntry(table, key) {
|
|
124
|
+
const entries = new Map(table.entries);
|
|
125
|
+
entries.delete(key);
|
|
126
|
+
return { entries, onMiss: table.onMiss, registry: table.registry, shape: 'keyed' };
|
|
127
|
+
}
|
|
128
|
+
// Binds `key` to `value`, returning a REPLACEMENT table. Not `setRegistryTableEntry`: this mutates
|
|
129
|
+
// nothing, and `set*` is reserved for in-place mutation. The owner assigns the result:
|
|
130
|
+
// registries.textureResolvers = withRegistryTableEntry(registries.textureResolvers, kind, resolver);
|
|
131
|
+
export function withRegistryTableEntry(table, key, value) {
|
|
132
|
+
const entries = new Map(table.entries);
|
|
133
|
+
entries.set(key, { state: RegistryEntryState.Bound, value });
|
|
134
|
+
return { entries, onMiss: table.onMiss, registry: table.registry, shape: 'keyed' };
|
|
135
|
+
}
|
|
136
|
+
// OMIT. Binds `key` to the tombstone: this table has an opinion about `key`, and the opinion is NOTHING.
|
|
137
|
+
// Under composition the overlay's tombstone WINS and the base's entry does not survive.
|
|
138
|
+
//
|
|
139
|
+
// The opposite of `withoutRegistryTableEntry`, which reads almost identically in English and composes the
|
|
140
|
+
// other way. Both leave `getRegistryTableEntry` answering `null` on a table with no base, so the
|
|
141
|
+
// difference is invisible until the table is composed. Choose by what you MEAN — override-with-nothing,
|
|
142
|
+
// or decline-to-override — never by which looks right at the call site.
|
|
143
|
+
export function withRegistryTableTombstone(table, key) {
|
|
144
|
+
const entries = new Map(table.entries);
|
|
145
|
+
entries.set(key, { state: RegistryEntryState.Tombstoned });
|
|
146
|
+
return { entries, onMiss: table.onMiss, registry: table.registry, shape: 'keyed' };
|
|
147
|
+
}
|
|
148
|
+
// The stored entry for `key`, tombstones included — the shape-dispatch every resolution query shares.
|
|
149
|
+
// Deliberately not exported: entries are composition currency, and handing one to a caller that only
|
|
150
|
+
// wanted a value is how a tombstone reaches somewhere that has never heard of one.
|
|
151
|
+
function getRegistryTableEntryState(table, key) {
|
|
152
|
+
if (table.shape === 'keyed')
|
|
153
|
+
return table.entries.get(key) ?? null;
|
|
154
|
+
if (table.shape === 'slot')
|
|
155
|
+
return key === table.registry ? table.entry : null;
|
|
156
|
+
const ordinal = table.vocabulary.indexOf(key);
|
|
157
|
+
if (ordinal === -1)
|
|
158
|
+
return null;
|
|
159
|
+
const value = table.entries[ordinal];
|
|
160
|
+
return value === null ? null : { state: RegistryEntryState.Bound, value };
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=registryTable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryTable.js","sourceRoot":"","sources":["../src/registryTable.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/registry",
|
|
3
|
+
"version": "0.3.1-next.1041.35b950c",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
9
|
+
"directory": "packages/registry"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./contract": {
|
|
20
|
+
"types": "./dist/contract.d.ts",
|
|
21
|
+
"default": "./dist/contract.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src/**/*.test.ts",
|
|
27
|
+
"!dist/**/*.test.js",
|
|
28
|
+
"!dist/**/*.test.d.ts",
|
|
29
|
+
"!dist/**/*.test.js.map",
|
|
30
|
+
"!dist/**/*.test.d.ts.map"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -b",
|
|
34
|
+
"clean": "tsc -b --clean",
|
|
35
|
+
"test": "vitest run --config vitest.config.ts",
|
|
36
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
37
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
38
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@flighthq/types": "0.3.1-next.1041.35b950c"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.3.0"
|
|
45
|
+
},
|
|
46
|
+
"description": "Registry tables — persistent keyed/slot/ordinal registries with explicit omission",
|
|
47
|
+
"sideEffects": false
|
|
48
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { KeyedTable, SlotTable } from '@flighthq/types/contract';
|
|
2
|
+
import { RegistryEntryState } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
concatRegistryTable,
|
|
6
|
+
createKeyedTable,
|
|
7
|
+
createOrdinalTable,
|
|
8
|
+
createSlotTable,
|
|
9
|
+
getOrdinalTableEntry,
|
|
10
|
+
getRegistryTableEntry,
|
|
11
|
+
getRegistryTableKeys,
|
|
12
|
+
hasRegistryTableEntry,
|
|
13
|
+
withoutRegistryTableEntry,
|
|
14
|
+
withRegistryTableEntry,
|
|
15
|
+
withRegistryTableTombstone,
|
|
16
|
+
} from './registryTable';
|
|
17
|
+
|
|
18
|
+
describe('concatRegistryTable', () => {
|
|
19
|
+
it('lets a bound overlay entry win over the base', () => {
|
|
20
|
+
const base = withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'base');
|
|
21
|
+
const overlay = withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'overlay');
|
|
22
|
+
|
|
23
|
+
expect(getRegistryTableEntry(concatRegistryTable(base, overlay), 'a')).toBe('overlay');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('carries a TOMBSTONE through as a tombstone, so the omitted base entry does NOT come back', () => {
|
|
27
|
+
// The defect this arm exists to prevent: resolving a tombstone to a binding during composition
|
|
28
|
+
// resurrects exactly the entry the overlay meant to omit.
|
|
29
|
+
const base = withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'base');
|
|
30
|
+
const overlay = withRegistryTableTombstone(createKeyedTable<string>('r', 'none'), 'a');
|
|
31
|
+
const composed = concatRegistryTable(base, overlay) as KeyedTable<string>;
|
|
32
|
+
|
|
33
|
+
expect(getRegistryTableEntry(composed, 'a')).toBeNull();
|
|
34
|
+
// And it is a tombstone in the result, not an absence — so composing again still omits.
|
|
35
|
+
expect(composed.entries.get('a')).toEqual({ state: RegistryEntryState.Tombstoned });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('INHERITS the base where the overlay merely has no opinion — the opposite of a tombstone', () => {
|
|
39
|
+
// withoutRegistryTableEntry and withRegistryTableTombstone read almost identically and compose
|
|
40
|
+
// oppositely. This pins the difference; without it the two verbs are interchangeable.
|
|
41
|
+
const base = withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'base');
|
|
42
|
+
const overlay = withoutRegistryTableEntry(
|
|
43
|
+
withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'x'),
|
|
44
|
+
'a',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(getRegistryTableEntry(concatRegistryTable(base, overlay), 'a')).toBe('base');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('throws on a shape mismatch, which is a programmer error rather than an expected failure', () => {
|
|
51
|
+
const keyed = createKeyedTable<string>('r', 'none');
|
|
52
|
+
const slot = createSlotTable<string>('r', 'none');
|
|
53
|
+
|
|
54
|
+
expect(() => concatRegistryTable(keyed, slot)).toThrow(/cannot compose a 'keyed' table with a 'slot' table/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('throws when the two tables are different registries', () => {
|
|
58
|
+
expect(() =>
|
|
59
|
+
concatRegistryTable(createKeyedTable<string>('a', 'none'), createKeyedTable<string>('b', 'none')),
|
|
60
|
+
).toThrow(/cannot compose registry 'a' with registry 'b'/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('throws when the two tables carry a different MISS POLICY', () => {
|
|
64
|
+
// The third refusal, alongside shape and registry id. Composition only ever compares policies for
|
|
65
|
+
// equality — it never interprets one — which is why the table layer needs no policy vocabulary and
|
|
66
|
+
// the type is an open alias. Same registry and same shape here, so the policy is the only difference.
|
|
67
|
+
expect(() =>
|
|
68
|
+
concatRegistryTable(createKeyedTable<string>('r', 'fallback'), createKeyedTable<string>('r', 'none')),
|
|
69
|
+
).toThrow(/cannot compose miss policy 'fallback' with miss policy 'none'/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('preserves the miss policy through composition and through every replacement', () => {
|
|
73
|
+
// A policy that survives concat but is dropped by withEntry would make the refusal above fire
|
|
74
|
+
// spuriously on the NEXT compose — so the preservation is pinned here rather than assumed.
|
|
75
|
+
const base = withRegistryTableEntry(createKeyedTable<string>('r', 'fallback'), 'a', 'v');
|
|
76
|
+
const overlay = withRegistryTableTombstone(createKeyedTable<string>('r', 'fallback'), 'b');
|
|
77
|
+
|
|
78
|
+
expect(base.onMiss).toBe('fallback');
|
|
79
|
+
expect(withoutRegistryTableEntry(base, 'a').onMiss).toBe('fallback');
|
|
80
|
+
expect((concatRegistryTable(base, overlay) as KeyedTable<string>).onMiss).toBe('fallback');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('composes a slot, where null is inherit and a tombstone is omit', () => {
|
|
84
|
+
const base: SlotTable<string> = {
|
|
85
|
+
entry: { state: RegistryEntryState.Bound, value: 'base' },
|
|
86
|
+
onMiss: 'none',
|
|
87
|
+
registry: 'r',
|
|
88
|
+
shape: 'slot',
|
|
89
|
+
};
|
|
90
|
+
const noOpinion = createSlotTable<string>('r', 'none');
|
|
91
|
+
const omit: SlotTable<string> = {
|
|
92
|
+
entry: { state: RegistryEntryState.Tombstoned },
|
|
93
|
+
onMiss: 'none',
|
|
94
|
+
registry: 'r',
|
|
95
|
+
shape: 'slot',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
expect(getRegistryTableEntry(concatRegistryTable(base, noOpinion), 'r')).toBe('base');
|
|
99
|
+
expect(getRegistryTableEntry(concatRegistryTable(base, omit), 'r')).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('createKeyedTable', () => {
|
|
104
|
+
it('starts empty and carries its registry id', () => {
|
|
105
|
+
const table = createKeyedTable<string>('textureResolvers', 'none');
|
|
106
|
+
|
|
107
|
+
expect(table.registry).toBe('textureResolvers');
|
|
108
|
+
expect(table.entries.size).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('createOrdinalTable', () => {
|
|
113
|
+
it('starts with every ordinal unbound, one per vocabulary entry', () => {
|
|
114
|
+
const table = createOrdinalTable<string>('swfTags', 'none', ['Tag0', 'Tag1', 'Tag2']);
|
|
115
|
+
|
|
116
|
+
expect(table.entries.length).toBe(3);
|
|
117
|
+
expect(table.entries.every((entry) => entry === null)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('createSlotTable', () => {
|
|
122
|
+
it('starts with no opinion, which is not the same as an explicit omission', () => {
|
|
123
|
+
expect(createSlotTable<string>('shapeRasterizer', 'none').entry).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('getOrdinalTableEntry', () => {
|
|
128
|
+
it('returns null out of range, which is the format skip path rather than a miss', () => {
|
|
129
|
+
const table = createOrdinalTable<string>('swfTags', 'none', ['Tag0']);
|
|
130
|
+
|
|
131
|
+
expect(getOrdinalTableEntry(table, 1)).toBeNull();
|
|
132
|
+
expect(getOrdinalTableEntry(table, -1)).toBeNull();
|
|
133
|
+
expect(getOrdinalTableEntry(table, 1.5)).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('indexes directly when the ordinal is in range', () => {
|
|
137
|
+
const table = createOrdinalTable<string>('swfTags', 'none', ['Tag0', 'Tag1']);
|
|
138
|
+
const bound = { ...table, entries: ['zero', null] };
|
|
139
|
+
|
|
140
|
+
expect(getOrdinalTableEntry(bound, 0)).toBe('zero');
|
|
141
|
+
expect(getOrdinalTableEntry(bound, 1)).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('getRegistryTableEntry', () => {
|
|
146
|
+
it('resolves a bound value', () => {
|
|
147
|
+
expect(getRegistryTableEntry(withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'v'), 'a')).toBe(
|
|
148
|
+
'v',
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('collapses a tombstone to null, because at resolution a tombstone IS a miss', () => {
|
|
153
|
+
expect(
|
|
154
|
+
getRegistryTableEntry(withRegistryTableTombstone(createKeyedTable<string>('r', 'none'), 'a'), 'a'),
|
|
155
|
+
).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns null for a key nobody has an opinion about', () => {
|
|
159
|
+
expect(getRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a')).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('getRegistryTableKeys', () => {
|
|
164
|
+
it('lists ONLY bound keys, so enumeration and resolution cannot disagree', () => {
|
|
165
|
+
// The trap: listing a tombstoned key means a caller that enumerates then resolves gets null for a
|
|
166
|
+
// key this function just said was there.
|
|
167
|
+
const table = withRegistryTableTombstone(
|
|
168
|
+
withRegistryTableEntry(withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'b', '1'), 'a', '2'),
|
|
169
|
+
'c',
|
|
170
|
+
);
|
|
171
|
+
const out: string[] = [];
|
|
172
|
+
getRegistryTableKeys(out, table);
|
|
173
|
+
|
|
174
|
+
expect(out).toEqual(['a', 'b']);
|
|
175
|
+
for (const key of out) expect(getRegistryTableEntry(table, key)).not.toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('clears `out` first, so a reused array does not accumulate', () => {
|
|
179
|
+
const out = ['stale'];
|
|
180
|
+
getRegistryTableKeys(out, createKeyedTable<string>('r', 'none'));
|
|
181
|
+
|
|
182
|
+
expect(out).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('names an ordinal table by vocabulary and a slot by its registry id', () => {
|
|
186
|
+
const ordinal = createOrdinalTable<string>('swfTags', 'none', ['Tag0', 'Tag1']);
|
|
187
|
+
const outOrdinal: string[] = [];
|
|
188
|
+
getRegistryTableKeys(outOrdinal, { ...ordinal, entries: [null, 'one'] });
|
|
189
|
+
expect(outOrdinal).toEqual(['Tag1']);
|
|
190
|
+
|
|
191
|
+
const outSlot: string[] = [];
|
|
192
|
+
getRegistryTableKeys(outSlot, {
|
|
193
|
+
entry: { state: RegistryEntryState.Bound, value: 'x' },
|
|
194
|
+
onMiss: 'none',
|
|
195
|
+
registry: 'shapeRasterizer',
|
|
196
|
+
shape: 'slot',
|
|
197
|
+
});
|
|
198
|
+
expect(outSlot).toEqual(['shapeRasterizer']);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('hasRegistryTableEntry', () => {
|
|
203
|
+
it('is FALSE on a tombstone, matching what get answers', () => {
|
|
204
|
+
const table = withRegistryTableTombstone(createKeyedTable<string>('r', 'none'), 'a');
|
|
205
|
+
|
|
206
|
+
expect(hasRegistryTableEntry(table, 'a')).toBe(false);
|
|
207
|
+
expect(getRegistryTableEntry(table, 'a')).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('is true for a bound key and false for an unknown one', () => {
|
|
211
|
+
const table = withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'v');
|
|
212
|
+
|
|
213
|
+
expect(hasRegistryTableEntry(table, 'a')).toBe(true);
|
|
214
|
+
expect(hasRegistryTableEntry(table, 'b')).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('withoutRegistryTableEntry', () => {
|
|
219
|
+
it('leaves NO opinion — the key is absent, not tombstoned', () => {
|
|
220
|
+
const table = withoutRegistryTableEntry(
|
|
221
|
+
withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'v'),
|
|
222
|
+
'a',
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(table.entries.has('a')).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('does not mutate the table it was given', () => {
|
|
229
|
+
const before = withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', 'v');
|
|
230
|
+
withoutRegistryTableEntry(before, 'a');
|
|
231
|
+
|
|
232
|
+
expect(getRegistryTableEntry(before, 'a')).toBe('v');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('withRegistryTableEntry', () => {
|
|
237
|
+
it('returns a REPLACEMENT and mutates nothing — the persistence contract', () => {
|
|
238
|
+
// This is why it is not named set*: the input is untouched and the caller must assign the result.
|
|
239
|
+
const before = createKeyedTable<string>('r', 'none');
|
|
240
|
+
const after = withRegistryTableEntry(before, 'a', 'v');
|
|
241
|
+
|
|
242
|
+
expect(before.entries.size).toBe(0);
|
|
243
|
+
expect(getRegistryTableEntry(after, 'a')).toBe('v');
|
|
244
|
+
expect(after).not.toBe(before);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('is last-write-wins for a key', () => {
|
|
248
|
+
const table = withRegistryTableEntry(
|
|
249
|
+
withRegistryTableEntry(createKeyedTable<string>('r', 'none'), 'a', '1'),
|
|
250
|
+
'a',
|
|
251
|
+
'2',
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(getRegistryTableEntry(table, 'a')).toBe('2');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('withRegistryTableTombstone', () => {
|
|
259
|
+
it('stores an explicit omission rather than removing the key', () => {
|
|
260
|
+
const table = withRegistryTableTombstone(createKeyedTable<string>('r', 'none'), 'a');
|
|
261
|
+
|
|
262
|
+
expect(table.entries.has('a')).toBe(true);
|
|
263
|
+
expect(table.entries.get('a')).toEqual({ state: RegistryEntryState.Tombstoned });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('does not mutate the table it was given', () => {
|
|
267
|
+
const before = createKeyedTable<string>('r', 'none');
|
|
268
|
+
withRegistryTableTombstone(before, 'a');
|
|
269
|
+
|
|
270
|
+
expect(before.entries.size).toBe(0);
|
|
271
|
+
});
|
|
272
|
+
});
|