@bjornpagen/bumbledb 1.0.1 → 1.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.
Files changed (172) hide show
  1. package/COOKBOOK.md +176 -36
  2. package/README.md +70 -26
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +28 -6
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +119 -71
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +8 -14
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +113 -188
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +17 -18
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +41 -20
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +76 -41
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +61 -26
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +152 -85
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +6 -4
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +3 -1
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +11 -11
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +2 -3
  63. package/dist/migration.d.ts.map +1 -1
  64. package/dist/migration.js +5 -5
  65. package/dist/migration.js.map +1 -1
  66. package/dist/native.d.ts +2 -2
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js.map +1 -1
  69. package/dist/query/atom.d.ts +1 -1
  70. package/dist/query/atom.d.ts.map +1 -1
  71. package/dist/query/atom.js.map +1 -1
  72. package/dist/query/description.d.ts +33 -0
  73. package/dist/query/description.d.ts.map +1 -0
  74. package/dist/query/description.js +302 -0
  75. package/dist/query/description.js.map +1 -0
  76. package/dist/query/lower.d.ts +60 -4
  77. package/dist/query/lower.d.ts.map +1 -1
  78. package/dist/query/lower.js +126 -161
  79. package/dist/query/lower.js.map +1 -1
  80. package/dist/query/parse-ir.d.ts +7 -2
  81. package/dist/query/parse-ir.d.ts.map +1 -1
  82. package/dist/query/parse-ir.js +240 -53
  83. package/dist/query/parse-ir.js.map +1 -1
  84. package/dist/query/run.d.ts.map +1 -1
  85. package/dist/query/run.js +8 -10
  86. package/dist/query/run.js.map +1 -1
  87. package/dist/query/scope.d.ts +3 -4
  88. package/dist/query/scope.d.ts.map +1 -1
  89. package/dist/query/scope.js +5 -10
  90. package/dist/query/scope.js.map +1 -1
  91. package/dist/relation.d.ts +22 -48
  92. package/dist/relation.d.ts.map +1 -1
  93. package/dist/relation.js +30 -82
  94. package/dist/relation.js.map +1 -1
  95. package/dist/result.d.ts +10 -25
  96. package/dist/result.d.ts.map +1 -1
  97. package/dist/result.js +7 -12
  98. package/dist/result.js.map +1 -1
  99. package/dist/rows.d.ts +11 -14
  100. package/dist/rows.d.ts.map +1 -1
  101. package/dist/rows.js +49 -237
  102. package/dist/rows.js.map +1 -1
  103. package/dist/runtime-codes.d.ts +1 -1
  104. package/dist/runtime-codes.d.ts.map +1 -1
  105. package/dist/runtime-codes.js +1 -2
  106. package/dist/runtime-codes.js.map +1 -1
  107. package/dist/runtime-errors.d.ts +27 -15
  108. package/dist/runtime-errors.d.ts.map +1 -1
  109. package/dist/runtime-errors.js +44 -8
  110. package/dist/runtime-errors.js.map +1 -1
  111. package/dist/runtime-native.d.ts +16 -34
  112. package/dist/runtime-native.d.ts.map +1 -1
  113. package/dist/runtime-native.js.map +1 -1
  114. package/dist/runtime.d.ts +11 -28
  115. package/dist/runtime.d.ts.map +1 -1
  116. package/dist/runtime.js +14 -54
  117. package/dist/runtime.js.map +1 -1
  118. package/dist/schema.d.ts +9 -1
  119. package/dist/schema.d.ts.map +1 -1
  120. package/dist/schema.js +85 -24
  121. package/dist/schema.js.map +1 -1
  122. package/dist/selection.d.ts +27 -0
  123. package/dist/selection.d.ts.map +1 -0
  124. package/dist/selection.js +104 -0
  125. package/dist/selection.js.map +1 -0
  126. package/dist/shape.d.ts +6 -9
  127. package/dist/shape.d.ts.map +1 -1
  128. package/dist/statements.d.ts +17 -52
  129. package/dist/statements.d.ts.map +1 -1
  130. package/dist/statements.js +92 -111
  131. package/dist/statements.js.map +1 -1
  132. package/dist/values.d.ts +25 -0
  133. package/dist/values.d.ts.map +1 -0
  134. package/dist/values.js +203 -0
  135. package/dist/values.js.map +1 -0
  136. package/pack-provenance.json +3 -3
  137. package/package.json +4 -4
  138. package/src/capacity.ts +86 -76
  139. package/src/changes.ts +225 -94
  140. package/src/closed.ts +161 -314
  141. package/src/codec.ts +123 -213
  142. package/src/compile.ts +61 -38
  143. package/src/db-native.ts +73 -68
  144. package/src/db.ts +268 -165
  145. package/src/errors.ts +16 -1
  146. package/src/face.ts +44 -74
  147. package/src/fields.ts +100 -146
  148. package/src/immutable.ts +76 -0
  149. package/src/index.ts +38 -31
  150. package/src/internal/log.ts +4 -5
  151. package/src/law.ts +14 -14
  152. package/src/lower.ts +13 -10
  153. package/src/migration.ts +5 -12
  154. package/src/native.ts +2 -1
  155. package/src/query/atom.ts +1 -0
  156. package/src/query/description.ts +394 -0
  157. package/src/query/lower.ts +139 -173
  158. package/src/query/parse-ir.ts +247 -54
  159. package/src/query/run.ts +15 -9
  160. package/src/query/scope.ts +10 -18
  161. package/src/relation.ts +58 -142
  162. package/src/result.ts +16 -45
  163. package/src/rows.ts +57 -250
  164. package/src/runtime-codes.ts +1 -2
  165. package/src/runtime-errors.ts +48 -8
  166. package/src/runtime-native.ts +13 -46
  167. package/src/runtime.ts +51 -119
  168. package/src/schema.ts +99 -29
  169. package/src/selection.ts +124 -0
  170. package/src/shape.ts +9 -9
  171. package/src/statements.ts +138 -189
  172. package/src/values.ts +223 -0
package/src/closed.ts CHANGED
@@ -1,364 +1,211 @@
1
1
  import { AuthoringError } from "#errors.ts"
2
2
  import {
3
- type AnyClosedIdField,
4
3
  type AnyField,
5
4
  assertDeclarationOrderKey,
6
5
  assertDeclarationRecord,
7
6
  type ClosedHandleTuple,
8
7
  type ClosedIdField,
9
- type ClosedRoster,
10
8
  type Infer,
11
- literalOf
9
+ ownHandles,
10
+ signaturesAgree
12
11
  } from "#fields.ts"
13
- import type { AnyRelation, RelationField } from "#relation.ts"
14
- import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
15
- import type { LiteralSpec } from "#spec.ts"
12
+ import { descriptorCache } from "#immutable.ts"
13
+ import {
14
+ type AnyRelation,
15
+ type FieldsShape,
16
+ ownFields,
17
+ type RelationField,
18
+ relationDescriptor,
19
+ relationFields
20
+ } from "#relation.ts"
21
+ import { fieldValue, recordValue } from "#values.ts"
16
22
 
17
23
  type PayloadField = AnyField
18
-
19
- type PayloadColumns = Record<string, PayloadField> & { readonly id?: never }
20
-
21
- interface ClosedColumn {
22
- readonly name: string
23
- readonly field: PayloadField
24
- }
25
-
26
- interface ClosedRow {
27
- readonly handle: string
28
- readonly values: readonly LiteralSpec[]
29
- }
30
-
31
- interface ClosedData {
32
- readonly name: string
33
- readonly handles: readonly string[]
34
- readonly columns: readonly ClosedColumn[]
35
- readonly rows: readonly ClosedRow[]
36
- }
37
-
38
- type AxiomRow<Cols extends Record<string, PayloadField>> = { readonly [C in keyof Cols]: Infer<Cols[C]> }
39
-
40
- type Axioms<Handles extends string, Cols extends Record<string, PayloadField>> = {
41
- readonly [H in Handles]: AxiomRow<Cols>
42
- }
43
-
44
- interface ClosedCore<
45
- Name extends string,
46
- Handles extends ClosedHandleTuple,
47
- Cols extends Record<string, PayloadField>
24
+ type PayloadColumns = FieldsShape & { readonly id?: never }
25
+ type AxiomRow<Cols extends FieldsShape> = { readonly [C in keyof Cols]: Infer<Cols[C]> }
26
+ type Axioms<Handles extends string, Cols extends FieldsShape> = { readonly [H in Handles]: AxiomRow<Cols> }
27
+
28
+ /** Closed relations declare their finite roster and its complete, typed ground facts. */
29
+ interface Closed<
30
+ Name extends string = string,
31
+ Handles extends ClosedHandleTuple = ClosedHandleTuple,
32
+ Cols extends FieldsShape = FieldsShape
48
33
  > {
34
+ readonly kind: "closed"
49
35
  readonly name: Name
50
-
51
- readonly id: ClosedIdField<Name, Handles>
52
- readonly data: ClosedData
53
-
54
- readonly axioms: Axioms<Handles[number], Cols>
55
-
36
+ readonly handles: Handles
56
37
  readonly columns: Cols
38
+ readonly axioms: Axioms<Handles[number], Cols>
57
39
  }
58
-
59
- type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = SelectionInput<Cols>
60
-
61
- interface SelectedClosed<
62
- Name extends string,
63
- Handles extends ClosedHandleTuple,
64
- Cols extends Record<string, PayloadField>
65
- > {
66
- readonly relation: Closed<Name, Handles, Cols>
67
- readonly selection: readonly SelectionBinding[]
68
- }
69
-
70
- interface AnySelectedClosed {
71
- readonly relation: AnyClosed
72
- readonly selection: readonly SelectionBinding[]
73
- }
74
-
75
- interface ClosedSelectable<
76
- Name extends string,
77
- Handles extends ClosedHandleTuple,
78
- Cols extends Record<string, PayloadField>
79
- > {
80
- where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols>
81
- }
82
-
83
- type Closed<Name extends string, Handles extends ClosedHandleTuple, Cols extends Record<string, PayloadField>> = [
84
- keyof Cols
85
- ] extends [never]
86
- ? ClosedCore<Name, Handles, Cols>
87
- : ClosedCore<Name, Handles, Cols> & ClosedSelectable<Name, Handles, Cols>
88
-
89
- interface AnyClosed {
90
- readonly name: string
91
- readonly id: AnyClosedIdField
92
- readonly data: ClosedData
93
- readonly axioms: Readonly<Record<string, object>>
94
- readonly columns: Readonly<Record<string, PayloadField>>
95
- }
40
+ type AnyClosed = Closed
96
41
 
97
42
  function isClosedMember(member: AnyRelation | AnyClosed): member is AnyClosed {
98
- return "handles" in member.data
43
+ return member.kind === "closed"
99
44
  }
100
45
 
101
- function sealedFieldsOf(member: AnyRelation | AnyClosed): readonly RelationField[] {
102
- if (isClosedMember(member)) {
103
- return Object.freeze([Object.freeze({ name: "id", field: member.id }), ...member.data.columns])
104
- }
105
- return member.data.fields
106
- }
107
-
108
- function sealedFieldOf(member: AnyRelation | AnyClosed, fieldName: string): AnyField | undefined {
109
- const declared = sealedFieldsOf(member).find(function byName(candidate) {
110
- return candidate.name === fieldName
46
+ /** The synthetic id is an ordinary field descriptor, derived from the declared roster. */
47
+ function closedId<C extends AnyClosed>(member: C): ClosedIdField<C["name"], C["handles"]> {
48
+ recordValue("closed relation", member, ["kind", "name", "handles", "columns", "axioms"])
49
+ assertDeclarationOrderKey("closed relation", member.name)
50
+ return Object.freeze({
51
+ kind: "u64",
52
+ closed: Object.freeze({ name: member.name, handles: ownHandles(`closed relation ${member.name}`, member.handles) })
111
53
  })
112
- return declared?.field
113
54
  }
114
55
 
115
- function isHandleTuple(shape: unknown): shape is ClosedHandleTuple {
116
- return (
117
- Array.isArray(shape) &&
118
- shape.length > 0 &&
119
- shape.every(function stringHandle(handle) {
120
- return typeof handle === "string"
121
- })
122
- )
56
+ function closedDescriptor<C extends AnyClosed>(input: C): C
57
+ function closedDescriptor(input: unknown): AnyClosed
58
+ function closedDescriptor(input: unknown): AnyClosed {
59
+ return checkedClosed(input)
123
60
  }
124
61
 
125
- /**
126
- * The trusted seam of the payload tier: the handle tuple and the axioms
127
- * record are the same finite set — every tuple name is an own key, and
128
- * every own key is in the tuple.
129
- */
130
- function handleKeysOwn<Handles extends string>(
131
- axioms: { readonly [H in Handles]: object },
132
- names: readonly Handles[]
133
- ): boolean {
134
- const listed = new Set<string>(names)
135
- return (
136
- names.every(function ownHandle(name) {
137
- return Object.hasOwn(axioms, name)
138
- }) &&
139
- Object.keys(axioms).every(function listedHandle(name) {
140
- return listed.has(name)
62
+ const checkedClosed = descriptorCache((input): AnyClosed => {
63
+ const raw = recordValue("closed relation", input, ["kind", "name", "handles", "columns", "axioms"])
64
+ if (raw.kind !== "closed" || typeof raw.name !== "string")
65
+ throw new AuthoringError({ message: "closed relation: expected a closed declaration" })
66
+ assertDeclarationOrderKey("closed relation", raw.name)
67
+ const handles = ownHandles(`closed relation ${raw.name}`, raw.handles)
68
+ const columns = ownFields(`closed relation ${raw.name} columns`, raw.columns)
69
+ if (Object.hasOwn(columns, "id"))
70
+ throw new AuthoringError({
71
+ message: `closed relation ${raw.name}: payload column id collides with the synthetic id`
72
+ })
73
+ const axioms = recordValue(`closed relation ${raw.name} axioms`, raw.axioms, handles)
74
+ const names = Object.keys(columns)
75
+ const owned = Object.fromEntries(
76
+ handles.map((handle) => {
77
+ const row = recordValue(`closed relation ${raw.name}.${handle}`, axioms[handle], names)
78
+ return [
79
+ handle,
80
+ Object.freeze(
81
+ Object.fromEntries(
82
+ Object.entries(columns).map(([name, field]) => [
83
+ name,
84
+ fieldValue(`closed relation ${raw.name}.${handle}.${name}`, field, row[name])
85
+ ])
86
+ )
87
+ )
88
+ ]
141
89
  })
142
90
  )
143
- }
144
-
145
- /**
146
- * The trusted seam of the axiom-readback mint: every handle carries an own
147
- * frozen row and every row carries every declared column as an own
148
- * property — verified before the record is admitted as the typed
149
- * {@link Axioms} (the trusted-admission-seam pattern — its home is
150
- * `isTypedScope` in query/lower.ts).
151
- */
152
- function axiomsMinted<Handles extends string, Cols extends Record<string, PayloadField>>(
153
- record: Readonly<Record<string, object>>,
154
- handles: readonly Handles[],
155
- cols: readonly ClosedColumn[]
156
- ): record is Axioms<Handles, Cols> & Readonly<Record<string, object>> {
157
- return handles.every(function rowMinted(handle) {
158
- const row = record[handle]
159
- return (
160
- row !== undefined &&
161
- cols.every(function columnMinted(column) {
162
- return Object.hasOwn(row, column.name)
163
- })
164
- )
165
- })
166
- }
167
-
168
- function mintAxioms<Handles extends string, Cols extends Record<string, PayloadField>>(
169
- name: string,
170
- handles: readonly Handles[],
171
- cols: readonly ClosedColumn[],
172
- axioms: Axioms<Handles, Cols>
173
- ): Axioms<Handles, Cols> {
174
- const out: Record<string, object> = {}
175
- for (const handle of handles) {
176
- const row = Object.freeze({ ...axioms[handle] })
177
- Object.defineProperty(out, handle, { value: row, enumerable: true })
178
- }
179
- Object.freeze(out)
180
- if (!axiomsMinted<Handles, Cols>(out, handles, cols)) {
181
- throw new AuthoringError({ message: `closed relation ${name}: axiom-row minting incomplete` })
182
- }
183
- return out
184
- }
91
+ return Object.freeze({ kind: "closed", name: raw.name, handles, columns, axioms: Object.freeze(owned) })
92
+ })
185
93
 
186
94
  function closed<const Name extends string, const Handles extends ClosedHandleTuple>(
187
95
  name: Name,
188
96
  handles: Handles
189
97
  ): Closed<Name, Handles, Record<never, never>>
190
-
191
98
  function closed<const Name extends string, const Handles extends ClosedHandleTuple, const Cols extends PayloadColumns>(
192
99
  name: Name,
193
100
  handles: Handles,
194
101
  columns: Cols,
195
102
  axioms: Axioms<Handles[number], Cols>
196
103
  ): Closed<Name, Handles, Cols>
197
-
198
- function closed<Name extends string, Handles extends ClosedHandleTuple, Cols extends PayloadColumns>(
199
- name: Name,
200
- handles: Handles,
201
- columns?: Cols,
202
- axioms?: Axioms<Handles[number], Cols>
203
- ): Closed<Name, Handles, Record<never, never>> | Closed<Name, Handles, Cols> {
204
- if (!Array.isArray(handles)) {
205
- throw new AuthoringError({
206
- message: `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, handles, columns, axioms)`
104
+ function closed(
105
+ name: string,
106
+ handles: ClosedHandleTuple,
107
+ columns?: PayloadColumns,
108
+ axioms?: Axioms<string, FieldsShape>
109
+ ): AnyClosed {
110
+ if (columns === undefined && axioms === undefined) {
111
+ const roster = ownHandles(`closed relation ${name}`, handles)
112
+ return closedDescriptor({
113
+ kind: "closed",
114
+ name,
115
+ handles: roster,
116
+ columns: {},
117
+ axioms: Object.fromEntries(roster.map((handle) => [handle, {}]))
207
118
  })
208
119
  }
209
- if (!isHandleTuple(handles)) {
120
+ if (columns === undefined || axioms === undefined)
210
121
  throw new AuthoringError({
211
- message: `closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`
122
+ message: `closed relation ${name}: payload columns and ground axioms must be supplied together`
212
123
  })
213
- }
214
- if (columns === undefined) {
215
- if (axioms !== undefined) {
216
- throw new AuthoringError({
217
- message: `closed relation ${name}: the bare tier declares no columns, so ground axioms are inadmissible`
218
- })
219
- }
220
- return closedBare(name, handles)
221
- }
222
- if (axioms === undefined) {
223
- throw new AuthoringError({
224
- message: `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, handles, columns, axioms)`
225
- })
226
- }
227
- return closedPayload(name, handles, columns, axioms)
228
- }
229
-
230
- function closedBare<Name extends string, const Hs extends ClosedHandleTuple>(
231
- name: Name,
232
- handles: Hs
233
- ): Closed<Name, Hs, Record<never, never>> {
234
- const empty: Record<string, object> = {}
235
- for (const handle of handles) {
236
- /** A duplicated name mints one row; the roster's own duplicate refusal in {@link mintClosed} stays the judge. */
237
- if (!Object.hasOwn(empty, handle)) {
238
- Object.defineProperty(empty, handle, { value: Object.freeze({}), enumerable: true })
239
- }
240
- }
241
- Object.freeze(empty)
242
- if (!axiomsMinted<Hs[number], Record<never, never>>(empty, handles, [])) {
243
- throw new AuthoringError({ message: `closed relation ${name}: bare-tier axiom-row minting incomplete` })
244
- }
245
- return mintClosed(name, handles, {}, empty)
246
- }
247
-
248
- function closedPayload<Name extends string, Handles extends ClosedHandleTuple, Cols extends PayloadColumns>(
249
- name: Name,
250
- handles: Handles,
251
- columns: Cols,
252
- axioms: Axioms<Handles[number], Cols>
253
- ): Closed<Name, Handles, Cols> {
254
- assertDeclarationRecord(`closed relation ${name} columns`, columns)
255
- for (const columnName of Object.keys(columns)) {
256
- assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
257
- }
258
- assertDeclarationRecord(`closed relation ${name} axioms`, axioms)
259
- for (const handle of handles) {
260
- assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
261
- }
262
- if (!handleKeysOwn(axioms, handles)) {
263
- throw new AuthoringError({ message: `closed relation ${name}: handle enumeration incomplete` })
264
- }
265
- return mintClosed(name, handles, columns, axioms)
124
+ return closedDescriptor({ kind: "closed", name, handles, columns, axioms })
266
125
  }
267
126
 
268
- function surfaceMinted<
269
- Name extends string,
270
- Handles extends ClosedHandleTuple,
271
- Cols extends Record<string, PayloadField>
272
- >(
273
- value: ClosedCore<Name, Handles, Cols>,
274
- cols: readonly ClosedColumn[]
275
- ): value is ClosedCore<Name, Handles, Cols> & Closed<Name, Handles, Cols> {
276
- const selectable = "where" in value && typeof value.where === "function"
277
- return cols.length > 0 ? selectable : !selectable
127
+ function memberDescriptor<M extends AnyRelation | AnyClosed>(input: M): M
128
+ function memberDescriptor(input: unknown): AnyRelation | AnyClosed
129
+ function memberDescriptor(input: unknown): AnyRelation | AnyClosed {
130
+ if (typeof input !== "object" || input === null)
131
+ throw new AuthoringError({ message: "relation: expected a declaration record" })
132
+ assertDeclarationRecord("relation", input)
133
+ if ("kind" in input && input.kind === "closed") return closedDescriptor(input)
134
+ return relationDescriptor(input)
278
135
  }
279
136
 
280
- function mintClosed<Name extends string, Handles extends ClosedHandleTuple, Cols extends Record<string, PayloadField>>(
281
- name: Name,
282
- handles: Handles,
283
- columns: Cols,
284
- axioms: Axioms<Handles[number], Cols>
285
- ): Closed<Name, Handles, Cols> {
286
- assertDeclarationOrderKey("closed relation", name)
287
- const seen = new Set<string>()
288
- for (const handle of handles) {
289
- if (seen.has(handle)) {
290
- throw new AuthoringError({ message: `closed relation ${name}: duplicate handle ${handle}` })
291
- }
292
- seen.add(handle)
293
- }
294
- const handleList = Object.freeze(handles)
295
- const roster: ClosedRoster<Name, Handles> = Object.freeze({ name, handles: handleList })
296
- const cols: ClosedColumn[] = []
297
- for (const [columnName, field] of Object.entries(columns)) {
298
- assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
299
- if (columnName === "id") {
300
- throw new AuthoringError({
301
- message: `closed relation ${name}: the payload column id collides with the sealed shape's synthetic id (the relation mints its own id at ordinal 0; name the column something else)`
302
- })
303
- }
304
- cols.push(Object.freeze({ name: columnName, field }))
305
- }
306
- Object.freeze(cols)
307
- const rows: ClosedRow[] = handleList.map(function lowerRow(handle: Handles[number]) {
308
- const row: Readonly<Record<string, unknown>> = axioms[handle]
309
- const values = cols.map(function lowerAxiomLiteral(column) {
310
- return Object.freeze(literalOf(column.field, row[column.name]))
311
- })
312
- return Object.freeze({ handle, values: Object.freeze(values) })
313
- })
314
- const data: ClosedData = Object.freeze({
315
- name,
316
- handles: roster.handles,
317
- columns: cols,
318
- rows: Object.freeze(rows)
319
- })
320
- const id: ClosedIdField<Name, Handles> = Object.freeze({ kind: "u64", closed: roster })
321
-
322
- const axiomsOut = mintAxioms<Handles[number], Cols>(name, handleList, cols, axioms)
323
- const columnsOut: Cols = { ...columns }
324
- Object.freeze(columnsOut)
325
- const holder: { value: Closed<Name, Handles, Cols> | undefined } = { value: undefined }
326
-
327
- function where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols> {
328
- const owner = holder.value
329
- if (owner === undefined) {
330
- throw new AuthoringError({
331
- message: `closed relation ${name}: self-reference read before construction completed`
332
- })
333
- }
334
- return Object.freeze({
335
- relation: owner,
336
- selection: resolveSelection(name, cols, Object.entries(selection))
137
+ function sealedFieldsOf(member: AnyRelation | AnyClosed): readonly RelationField[] {
138
+ return isClosedMember(member)
139
+ ? [
140
+ { name: "id", field: closedId(member) },
141
+ ...Object.entries(member.columns).map(([name, field]) => ({ name, field }))
142
+ ]
143
+ : relationFields(member)
144
+ }
145
+ function sealedFieldOf(member: AnyRelation | AnyClosed, name: string): AnyField | undefined {
146
+ if (isClosedMember(member)) {
147
+ if (name === "id") return closedId(member)
148
+ return Object.hasOwn(member.columns, name) ? member.columns[name] : undefined
149
+ }
150
+ return Object.hasOwn(member.fields, name) ? member.fields[name] : undefined
151
+ }
152
+
153
+ function sameValue(a: unknown, b: unknown): boolean {
154
+ if (Object.is(a, b)) return true
155
+ if (a instanceof Uint8Array && b instanceof Uint8Array)
156
+ return a.length === b.length && a.every((byte, i) => byte === b[i])
157
+ if (
158
+ typeof a === "object" &&
159
+ a !== null &&
160
+ typeof b === "object" &&
161
+ b !== null &&
162
+ "start" in a &&
163
+ "end" in a &&
164
+ "start" in b &&
165
+ "end" in b
166
+ )
167
+ return Object.is(a.start, b.start) && Object.is(a.end, b.end)
168
+ return false
169
+ }
170
+
171
+ /** Equal names must denote equal ordered declarations, never constructor identities. */
172
+ function membersAgree(left: AnyRelation | AnyClosed | undefined, right: AnyRelation | AnyClosed): boolean {
173
+ if (left === undefined) return false
174
+ const a = memberDescriptor(left)
175
+ const b = memberDescriptor(right)
176
+ if (a === b) return true
177
+ if (a.kind !== b.kind || a.name !== b.name) return false
178
+ const af = sealedFieldsOf(a)
179
+ const bf = sealedFieldsOf(b)
180
+ if (
181
+ af.length !== bf.length ||
182
+ !af.every((field, i) => {
183
+ const other = bf[i]
184
+ return other !== undefined && field.name === other.name && signaturesAgree(field.field, other.field)
337
185
  })
186
+ )
187
+ return false
188
+ if (isClosedMember(a) && isClosedMember(b)) {
189
+ return (
190
+ a.handles.length === b.handles.length &&
191
+ a.handles.every(
192
+ (handle, i) =>
193
+ handle === b.handles[i] &&
194
+ Object.keys(a.columns).every((name) => sameValue(a.axioms[handle]?.[name], b.axioms[handle]?.[name]))
195
+ )
196
+ )
338
197
  }
339
- const core = { name, id, data, axioms: axiomsOut, columns: columnsOut }
340
- const value: ClosedCore<Name, Handles, Cols> =
341
- cols.length > 0 ? Object.freeze({ ...core, where }) : Object.freeze(core)
342
- if (!surfaceMinted(value, cols)) {
343
- throw new AuthoringError({ message: `closed relation ${name}: ergonomic-surface minting incomplete` })
344
- }
345
- holder.value = value
346
- return value
198
+ return true
347
199
  }
348
200
 
349
- export type {
350
- AnyClosed,
351
- AnySelectedClosed,
352
- AxiomRow,
353
- Axioms,
354
- Closed,
355
- ClosedColumn,
356
- ClosedCore,
357
- ClosedData,
358
- ClosedRow,
359
- ClosedSelectable,
360
- ClosedSelectionInput,
361
- PayloadField,
362
- SelectedClosed
201
+ export type { AnyClosed, AxiomRow, Axioms, Closed, PayloadField }
202
+ export {
203
+ closed,
204
+ closedDescriptor,
205
+ closedId,
206
+ isClosedMember,
207
+ memberDescriptor,
208
+ membersAgree,
209
+ sealedFieldOf,
210
+ sealedFieldsOf
363
211
  }
364
- export { closed, isClosedMember, sealedFieldOf, sealedFieldsOf }