@elaraai/east-ui 1.0.74 → 1.0.76
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/src/collections/index.d.ts +1 -0
- package/dist/src/collections/index.d.ts.map +1 -1
- package/dist/src/collections/index.js +1 -0
- package/dist/src/collections/index.js.map +1 -1
- package/dist/src/collections/sheet/bridge.d.ts +265 -0
- package/dist/src/collections/sheet/bridge.d.ts.map +1 -0
- package/dist/src/collections/sheet/bridge.js +630 -0
- package/dist/src/collections/sheet/bridge.js.map +1 -0
- package/dist/src/collections/sheet/columns.d.ts +360 -0
- package/dist/src/collections/sheet/columns.d.ts.map +1 -0
- package/dist/src/collections/sheet/columns.js +99 -0
- package/dist/src/collections/sheet/columns.js.map +1 -0
- package/dist/src/collections/sheet/index.d.ts +207 -0
- package/dist/src/collections/sheet/index.d.ts.map +1 -0
- package/dist/src/collections/sheet/index.js +126 -0
- package/dist/src/collections/sheet/index.js.map +1 -0
- package/dist/src/collections/sheet/link.d.ts +392 -0
- package/dist/src/collections/sheet/link.d.ts.map +1 -0
- package/dist/src/collections/sheet/link.js +222 -0
- package/dist/src/collections/sheet/link.js.map +1 -0
- package/dist/src/collections/sheet/registers.d.ts +131 -0
- package/dist/src/collections/sheet/registers.d.ts.map +1 -0
- package/dist/src/collections/sheet/registers.js +119 -0
- package/dist/src/collections/sheet/registers.js.map +1 -0
- package/dist/src/collections/sheet/root.d.ts +179 -0
- package/dist/src/collections/sheet/root.d.ts.map +1 -0
- package/dist/src/collections/sheet/root.js +308 -0
- package/dist/src/collections/sheet/root.js.map +1 -0
- package/dist/src/collections/sheet/types.d.ts +6601 -0
- package/dist/src/collections/sheet/types.d.ts.map +1 -0
- package/dist/src/collections/sheet/types.js +744 -0
- package/dist/src/collections/sheet/types.js.map +1 -0
- package/dist/src/component.d.ts +2500 -0
- package/dist/src/component.d.ts.map +1 -1
- package/dist/src/component.js +6 -0
- package/dist/src/component.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/internal.d.ts +4 -2
- package/dist/src/internal.d.ts.map +1 -1
- package/dist/src/internal.js +5 -2
- package/dist/src/internal.js.map +1 -1
- package/dist/src/platform/index.d.ts +1 -1
- package/dist/src/platform/index.d.ts.map +1 -1
- package/dist/src/platform/index.js +1 -1
- package/dist/src/platform/index.js.map +1 -1
- package/dist/src/platform/slice/impl.d.ts +17 -1
- package/dist/src/platform/slice/impl.d.ts.map +1 -1
- package/dist/src/platform/slice/impl.js +60 -12
- package/dist/src/platform/slice/impl.js.map +1 -1
- package/dist/src/platform/slice/index.d.ts +155 -10
- package/dist/src/platform/slice/index.d.ts.map +1 -1
- package/dist/src/platform/slice/index.js +37 -17
- package/dist/src/platform/slice/index.js.map +1 -1
- package/dist/src/runtime/collections/index.d.ts +1 -0
- package/dist/src/runtime/collections/index.d.ts.map +1 -1
- package/dist/src/runtime/collections/index.js +1 -0
- package/dist/src/runtime/collections/index.js.map +1 -1
- package/dist/src/runtime/collections/sheet.d.ts +89 -0
- package/dist/src/runtime/collections/sheet.d.ts.map +1 -0
- package/dist/src/runtime/collections/sheet.js +65 -0
- package/dist/src/runtime/collections/sheet.js.map +1 -0
- package/dist/src/slice/index.d.ts +97 -1
- package/dist/src/slice/index.d.ts.map +1 -1
- package/dist/test/collections/sheet.examples.d.ts +56 -0
- package/dist/test/collections/sheet.examples.d.ts.map +1 -0
- package/dist/test/collections/sheet.examples.js +725 -0
- package/dist/test/collections/sheet.examples.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The typed bridge (`Sheet Spec.md` §4.8) — factory internals, both arms.
|
|
7
|
+
*
|
|
8
|
+
* Everything the author writes is typed over the host's row `R` and the
|
|
9
|
+
* driver's row `D`; everything on the wire is closed. This module joins the
|
|
10
|
+
* two with compiled East functions that capture only data, bind handles and
|
|
11
|
+
* the author's functions:
|
|
12
|
+
*
|
|
13
|
+
* - `projectRow` — `R` → the wire cells (the same projection the row source
|
|
14
|
+
* uses, so `encode` and `make` cannot disagree);
|
|
15
|
+
* - `rowById` — the REAL source row by id: the captured collection or bind
|
|
16
|
+
* handle on the inline arm, the source's `page` at the row's offset on the
|
|
17
|
+
* paged arm. Built ONCE per sheet and shared by every wrapper;
|
|
18
|
+
* - `decode` — the source row as the base (the row type's default when
|
|
19
|
+
* absent — an insert, a proposal), then each column's field from its cell
|
|
20
|
+
* by the static tag. A field with no column keeps the base row's value;
|
|
21
|
+
* - `bridgeCtx` — the wire context to `Sheet.Types.Context(R, D)`;
|
|
22
|
+
* - the wrappers — a fill provider, a proposer, an arity rule, a member
|
|
23
|
+
* check, a custom kind's parse / print, `onEdit` — each the author's typed
|
|
24
|
+
* function inside its closed wire twin.
|
|
25
|
+
*
|
|
26
|
+
* The bridge is the one place a string ever names a field: inside the
|
|
27
|
+
* factory, against the column list it has already validated.
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
import { East, Expr, ArrayType, AsyncFunctionType, BooleanType, DateTimeType, DictType, FloatType, FunctionType, IntegerType, NullType, OptionType, StringType, StructType, defaultValue, variant, some, none, } from "@elaraai/east";
|
|
32
|
+
import { SheetCellType, SheetLinkType, SheetContextType, SheetFillType, SheetProposalType, SheetProviderType, SheetProposerType, SheetCheckContextType, SheetCheckType, SheetCountedType, SheetEditType, SheetHalfType, SheetContextTypeFor, SheetCheckContextTypeFor, SheetEditTypeFor, SheetPatchTypeFor, SheetProposalTypeFor, } from "./types.js";
|
|
33
|
+
import { SheetMembersType, SheetRegisterMembersType, parseLink, printLink, printLinkWith, EMPTY_LINK } from "./link.js";
|
|
34
|
+
/** The wire cells dictionary type. */
|
|
35
|
+
export const SheetCellsType = DictType(StringType, SheetCellType);
|
|
36
|
+
/** The East type of an erased expression (`Expr.type` cannot infer through `ExprType<EastType>`). */
|
|
37
|
+
function typeOf(e) {
|
|
38
|
+
return Expr.type(e);
|
|
39
|
+
}
|
|
40
|
+
/** Whether a type is `Option<T>` — a variant of exactly `none: Null` and `some`. */
|
|
41
|
+
export function optionPayload(t) {
|
|
42
|
+
const v = t;
|
|
43
|
+
if (v.type !== "Variant" || v.cases === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
const keys = Object.keys(v.cases);
|
|
46
|
+
if (keys.length !== 2 || v.cases["none"] === undefined || v.cases["some"] === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (v.cases["none"].type !== "Null")
|
|
49
|
+
return undefined;
|
|
50
|
+
return v.cases["some"];
|
|
51
|
+
}
|
|
52
|
+
/** The cell arm a primitive payload lands in, or `undefined` for a type no cell holds. */
|
|
53
|
+
export function cellTagOf(t) {
|
|
54
|
+
if (t === SheetLinkType)
|
|
55
|
+
return "Link";
|
|
56
|
+
switch (t.type) {
|
|
57
|
+
case "String": return "String";
|
|
58
|
+
case "DateTime": return "DateTime";
|
|
59
|
+
case "Float": return "Float";
|
|
60
|
+
case "Integer": return "Integer";
|
|
61
|
+
case "Boolean": return "Boolean";
|
|
62
|
+
default: return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const SCALAR_PAYLOAD = {
|
|
66
|
+
text: StringType, lookup: StringType, reference: StringType, enum: StringType, stamped: StringType,
|
|
67
|
+
date: DateTimeType, quantity: FloatType, integer: IntegerType,
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Describe one column against the row type — the build-time refusals of
|
|
71
|
+
* §3.12 live here, every one naming the column and the remedy.
|
|
72
|
+
*
|
|
73
|
+
* @param key - The column key
|
|
74
|
+
* @param col - The built column
|
|
75
|
+
* @param rowType - The host's row type
|
|
76
|
+
* @returns The column's metadata
|
|
77
|
+
* @throws Error naming the column when the kind cannot sit on the field
|
|
78
|
+
*/
|
|
79
|
+
export function describeColumn(key, col, rowType) {
|
|
80
|
+
const fields = rowType.fields;
|
|
81
|
+
const fieldType = fields[key];
|
|
82
|
+
if (fieldType === undefined) {
|
|
83
|
+
throw new Error(`Sheet: column "${key}" is not a field of the row type — the columns map is keyed by the row's fields (${Object.keys(fields).join(", ")})`);
|
|
84
|
+
}
|
|
85
|
+
const cfg = col.config;
|
|
86
|
+
const kind = col.kind;
|
|
87
|
+
const payloadOf = optionPayload(fieldType);
|
|
88
|
+
const optional = payloadOf !== undefined;
|
|
89
|
+
const unwrapped = payloadOf ?? fieldType;
|
|
90
|
+
// A derived read — the projection decides the payload; the field is free.
|
|
91
|
+
if (cfg.value !== undefined && kind !== "set" && kind !== "link" && kind !== "stamped" && kind !== "custom") {
|
|
92
|
+
const payloadType = SCALAR_PAYLOAD[kind];
|
|
93
|
+
const derived = East.function([rowType], undefined, (_$, r) => cfg.value(r));
|
|
94
|
+
const out = Expr.type(derived).output;
|
|
95
|
+
const outPayload = optionPayload(out);
|
|
96
|
+
const outUnwrapped = outPayload ?? out;
|
|
97
|
+
if (outUnwrapped !== payloadType) {
|
|
98
|
+
throw new Error(`Sheet: column "${key}" is a ${kind} column whose \`value\` projection returns ${outUnwrapped.type} — a ${kind} column's projection must return ${payloadType.type} or its Option`);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
key, kind, config: cfg, fieldType, optional, payloadType,
|
|
102
|
+
cellTag: cellTagOf(payloadType), editable: false,
|
|
103
|
+
derived, derivedOptional: outPayload !== undefined,
|
|
104
|
+
...(col.register !== undefined ? { register: col.register } : {}),
|
|
105
|
+
...(col.driverType !== undefined ? { driverType: col.driverType } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (kind === "set" || kind === "link") {
|
|
109
|
+
let form;
|
|
110
|
+
let ownHalf;
|
|
111
|
+
let otherField;
|
|
112
|
+
if (fieldType === SheetLinkType || unwrapped === SheetLinkType) {
|
|
113
|
+
form = "link";
|
|
114
|
+
}
|
|
115
|
+
else if (fieldType === SheetMembersType) {
|
|
116
|
+
form = "array";
|
|
117
|
+
if (kind === "set") {
|
|
118
|
+
ownHalf = "to";
|
|
119
|
+
}
|
|
120
|
+
else if (cfg.to !== undefined && cfg.from === undefined) {
|
|
121
|
+
ownHalf = "from";
|
|
122
|
+
otherField = cfg.to;
|
|
123
|
+
}
|
|
124
|
+
else if (cfg.from !== undefined && cfg.to === undefined) {
|
|
125
|
+
ownHalf = "to";
|
|
126
|
+
otherField = cfg.from;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
throw new Error(`Sheet: link column "${key}" sits on an Array<Member> field, so it must name the OTHER half's field — \`to: "…"\` when this field holds the from members, \`from: "…"\` when it holds the to members`);
|
|
130
|
+
}
|
|
131
|
+
if (otherField !== undefined && fields[otherField] !== SheetMembersType) {
|
|
132
|
+
throw new Error(`Sheet: link column "${key}" names "${otherField}" as its other half, but that is not an Array<Sheet.Types.Member> field of the row`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (unwrapped.type === "String") {
|
|
136
|
+
form = "string";
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
throw new Error(`Sheet: ${kind} column "${key}" must sit on a Sheet.Types.Link, Array<Sheet.Types.Member> or String field — got ${fieldType.type}`);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
key, kind, config: cfg, fieldType, optional: form === "link" ? unwrapped === SheetLinkType && optional : optional,
|
|
143
|
+
payloadType: SheetLinkType, cellTag: "Link", editable: cfg.editable !== false,
|
|
144
|
+
form,
|
|
145
|
+
...(ownHalf !== undefined ? { ownHalf } : {}),
|
|
146
|
+
...(otherField !== undefined ? { otherField } : {}),
|
|
147
|
+
...(col.register !== undefined ? { register: col.register } : {}),
|
|
148
|
+
...(col.driverType !== undefined ? { driverType: col.driverType } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (kind === "custom") {
|
|
152
|
+
const parseFn = East.value(cfg.parse);
|
|
153
|
+
const parseType = typeOf(parseFn);
|
|
154
|
+
const parsed = optionPayload(parseType.output);
|
|
155
|
+
if (parsed === undefined) {
|
|
156
|
+
throw new Error(`Sheet: custom column "${key}" has a \`parse\` whose output is not an Option — it must return Option<payload>, \`none\` meaning unrecognised`);
|
|
157
|
+
}
|
|
158
|
+
if (parsed !== unwrapped) {
|
|
159
|
+
throw new Error(`Sheet: custom column "${key}" parses to ${parsed.type} but sits on a ${unwrapped.type} field — the payload must be the field's type (or its Option's)`);
|
|
160
|
+
}
|
|
161
|
+
const tag = cellTagOf(parsed);
|
|
162
|
+
if (tag === undefined) {
|
|
163
|
+
throw new Error(`Sheet: custom column "${key}" has payload ${parsed.type}, which no cell holds — use String, Integer, Float, DateTime, Boolean or Sheet.Types.Link`);
|
|
164
|
+
}
|
|
165
|
+
return { key, kind, config: cfg, fieldType, optional, payloadType: parsed, cellTag: tag, editable: cfg.editable !== false };
|
|
166
|
+
}
|
|
167
|
+
const payloadType = SCALAR_PAYLOAD[kind];
|
|
168
|
+
if (unwrapped !== payloadType) {
|
|
169
|
+
throw new Error(`Sheet: ${kind} column "${key}" must sit on a ${payloadType.type} or Option<${payloadType.type}> field — got ${fieldType.type}`);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
key, kind, config: cfg, fieldType, optional, payloadType,
|
|
173
|
+
cellTag: cellTagOf(payloadType),
|
|
174
|
+
editable: kind !== "stamped" && cfg.editable !== false,
|
|
175
|
+
...(col.register !== undefined ? { register: col.register } : {}),
|
|
176
|
+
...(col.driverType !== undefined ? { driverType: col.driverType } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Cells — payload ↔ cell (the static-tag moves)
|
|
181
|
+
// ============================================================================
|
|
182
|
+
/** A payload value as a cell. */
|
|
183
|
+
export function cellOfPayload(tag, v) {
|
|
184
|
+
return East.value(variant(tag, v), SheetCellType);
|
|
185
|
+
}
|
|
186
|
+
/** The blank cell. */
|
|
187
|
+
export const NULL_CELL = East.value(variant("Null", null), SheetCellType);
|
|
188
|
+
/** A field VALUE (the payload or its Option) as a cell, by the column's static shape. */
|
|
189
|
+
function cellOfFieldValue(meta, v, optional) {
|
|
190
|
+
if (!optional)
|
|
191
|
+
return cellOfPayload(meta.cellTag, v);
|
|
192
|
+
return v.match({
|
|
193
|
+
some: (_$, x) => cellOfPayload(meta.cellTag, x),
|
|
194
|
+
none: (_$) => NULL_CELL,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The cell of a column for one row — the projection `make` and `encode`
|
|
199
|
+
* share. `members` is the column's register (bound in the caller's block)
|
|
200
|
+
* for the `String` link form.
|
|
201
|
+
*/
|
|
202
|
+
function projectCell(meta, r, members) {
|
|
203
|
+
const row = r;
|
|
204
|
+
if (meta.derived !== undefined) {
|
|
205
|
+
return cellOfFieldValue(meta, meta.derived(r), meta.derivedOptional === true);
|
|
206
|
+
}
|
|
207
|
+
const fv = row[meta.key];
|
|
208
|
+
if (meta.form === undefined)
|
|
209
|
+
return cellOfFieldValue(meta, fv, meta.optional);
|
|
210
|
+
if (meta.form === "link")
|
|
211
|
+
return cellOfFieldValue(meta, fv, meta.optional);
|
|
212
|
+
if (meta.form === "array") {
|
|
213
|
+
const half = fv;
|
|
214
|
+
const empty = East.value([], SheetMembersType);
|
|
215
|
+
const other = meta.otherField !== undefined ? row[meta.otherField] : empty;
|
|
216
|
+
const link = meta.ownHalf === "from"
|
|
217
|
+
? East.value({ from: half, to: other }, SheetLinkType)
|
|
218
|
+
: East.value({ from: other, to: half }, SheetLinkType);
|
|
219
|
+
return cellOfPayload("Link", link);
|
|
220
|
+
}
|
|
221
|
+
// The String form — the grammar parses it on read; blank text is the blank cell.
|
|
222
|
+
const parse = (text) => text.trim().length().equal(0n).ifElse((_$) => NULL_CELL, (_$) => cellOfPayload("Link", parseLink(text, members ?? East.value([], SheetRegisterMembersType))));
|
|
223
|
+
if (!meta.optional)
|
|
224
|
+
return parse(fv);
|
|
225
|
+
return fv.match({
|
|
226
|
+
some: (_$, s) => parse(s),
|
|
227
|
+
none: (_$) => NULL_CELL,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** The field value a cell decodes to — the payload or its Option, by the column's static shape. */
|
|
231
|
+
function fieldValueOfCell(meta, cell, optional, payloadType) {
|
|
232
|
+
if (optional) {
|
|
233
|
+
return cell.match({ [meta.cellTag]: (_$, v) => East.value(some(v), OptionType(payloadType)) }, (_$) => East.value(none, OptionType(payloadType)));
|
|
234
|
+
}
|
|
235
|
+
return cell.match({ [meta.cellTag]: (_$, v) => v }, (_$) => East.value(defaultValue(payloadType), payloadType));
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The value of THIS column's field from its cell (the base field when the cell
|
|
239
|
+
* is absent or the column is read-only). `members` is the column's register,
|
|
240
|
+
* bound in the caller's block, for the String link form's canonical print.
|
|
241
|
+
*/
|
|
242
|
+
function decodeOwnField(meta, cellOpt, base, members) {
|
|
243
|
+
if (!meta.editable)
|
|
244
|
+
return base;
|
|
245
|
+
// The String form prints the link back through the grammar — the keys as
|
|
246
|
+
// typed, or the register's labels under `store: "canonical"` (B§4.2).
|
|
247
|
+
const print = (l) => meta.config.store === "canonical" && members !== undefined ? printLinkWith(l, members) : printLink(l);
|
|
248
|
+
return cellOpt.match({
|
|
249
|
+
none: (_$) => base,
|
|
250
|
+
some: (_$, cell) => {
|
|
251
|
+
if (meta.form === undefined)
|
|
252
|
+
return fieldValueOfCell(meta, cell, meta.optional, meta.payloadType);
|
|
253
|
+
if (meta.form === "link")
|
|
254
|
+
return fieldValueOfCell(meta, cell, meta.optional, SheetLinkType);
|
|
255
|
+
if (meta.form === "array") {
|
|
256
|
+
return cell.match({ Link: (_$2, l) => meta.ownHalf === "from" ? l.from : l.to }, (_$2) => East.value([], SheetMembersType));
|
|
257
|
+
}
|
|
258
|
+
if (meta.optional) {
|
|
259
|
+
return cell.match({ Link: (_$2, l) => East.value(some(print(l)), OptionType(StringType)) }, (_$2) => East.value(none, OptionType(StringType)));
|
|
260
|
+
}
|
|
261
|
+
return cell.match({ Link: (_$2, l) => print(l) }, (_$2) => East.value("", StringType));
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/** The OTHER half's field of an `array`-form link column, from that column's cell. */
|
|
266
|
+
function decodeOtherHalf(meta, cellOpt, base) {
|
|
267
|
+
if (!meta.editable)
|
|
268
|
+
return base;
|
|
269
|
+
return cellOpt.match({
|
|
270
|
+
none: (_$) => base,
|
|
271
|
+
some: (_$, cell) => cell.match({ Link: (_$2, l) => meta.ownHalf === "from" ? l.to : l.from }, (_$2) => East.value([], SheetMembersType)),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** The id of a row — the id field, or the key of a keyed window. */
|
|
275
|
+
function idOf(r, idField) {
|
|
276
|
+
if (idField === undefined)
|
|
277
|
+
throw new Error("Sheet: a positional source needs `id` — the String field that identifies a row");
|
|
278
|
+
return r[idField];
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Compile the bridge for one sheet.
|
|
282
|
+
*
|
|
283
|
+
* @param input - The root's resolved declaration
|
|
284
|
+
* @returns The bridge
|
|
285
|
+
*/
|
|
286
|
+
export function buildBridge(input) {
|
|
287
|
+
const { rowType, idField, metas, registers, driver, source } = input;
|
|
288
|
+
const driverType = driver !== undefined ? driver.rowType : NullType;
|
|
289
|
+
const ctxType = SheetContextTypeFor(rowType, driverType);
|
|
290
|
+
const checkCtxType = SheetCheckContextTypeFor(rowType);
|
|
291
|
+
const editType = SheetEditTypeFor(rowType);
|
|
292
|
+
const patchType = SheetPatchTypeFor(rowType);
|
|
293
|
+
const proposalType = SheetProposalTypeFor(rowType);
|
|
294
|
+
const fields = rowType.fields;
|
|
295
|
+
const byField = new Map(metas.map((m) => [m.key, m]));
|
|
296
|
+
const otherHalfBy = new Map();
|
|
297
|
+
for (const m of metas)
|
|
298
|
+
if (m.otherField !== undefined)
|
|
299
|
+
otherHalfBy.set(m.otherField, m);
|
|
300
|
+
// R → the wire cells. Registers are bound ONCE per body for the String
|
|
301
|
+
// link form (the capture rule: data, never a spliced expression).
|
|
302
|
+
const stringFormRegisters = [...new Set(metas.filter((m) => m.form === "string" && m.register !== undefined).map((m) => m.register))];
|
|
303
|
+
const projectRow = East.function([rowType], SheetCellsType, ($, r) => {
|
|
304
|
+
const cells = $.let(new Map(), SheetCellsType);
|
|
305
|
+
const bound = new Map();
|
|
306
|
+
for (const name of stringFormRegisters) {
|
|
307
|
+
bound.set(name, $.const(registers[name], SheetRegisterMembersType));
|
|
308
|
+
}
|
|
309
|
+
for (const m of metas) {
|
|
310
|
+
$(cells.insert(m.key, projectCell(m, r, m.register !== undefined ? bound.get(m.register) : undefined)));
|
|
311
|
+
}
|
|
312
|
+
return cells;
|
|
313
|
+
});
|
|
314
|
+
// (id, cells, base) → R: the base row (the type's default when absent),
|
|
315
|
+
// every column's field from its cell, the id field from the wire id.
|
|
316
|
+
const decode = East.function([StringType, SheetCellsType, OptionType(rowType)], rowType, ($, id, cells, base) => {
|
|
317
|
+
const b = $.let(base.match({
|
|
318
|
+
some: (_$, x) => x,
|
|
319
|
+
none: (_$) => East.value(defaultValue(rowType), rowType),
|
|
320
|
+
}), rowType);
|
|
321
|
+
// The String link form's registers, bound once per body (the capture rule).
|
|
322
|
+
const bound = new Map();
|
|
323
|
+
for (const name of stringFormRegisters) {
|
|
324
|
+
bound.set(name, $.const(registers[name], SheetRegisterMembersType));
|
|
325
|
+
}
|
|
326
|
+
const out = {};
|
|
327
|
+
for (const f of Object.keys(fields)) {
|
|
328
|
+
const baseField = $.let(b[f], fields[f]);
|
|
329
|
+
if (f === idField) {
|
|
330
|
+
out[f] = id;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const own = byField.get(f);
|
|
334
|
+
if (own !== undefined) {
|
|
335
|
+
out[f] = decodeOwnField(own, cells.tryGet(own.key), baseField, own.register !== undefined ? bound.get(own.register) : undefined);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const other = otherHalfBy.get(f);
|
|
339
|
+
if (other !== undefined) {
|
|
340
|
+
out[f] = decodeOtherHalf(other, cells.tryGet(other.key), baseField);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
out[f] = baseField;
|
|
344
|
+
}
|
|
345
|
+
return East.value(out, rowType);
|
|
346
|
+
});
|
|
347
|
+
// (id, offset) → the real source row.
|
|
348
|
+
const rowById = source.kind === "inline"
|
|
349
|
+
? East.function([StringType, IntegerType], OptionType(rowType), ($, id, _offset) => {
|
|
350
|
+
const rows = $.const(source.rows, ArrayType(rowType));
|
|
351
|
+
return rows.firstMap((_$, r) => idOf(r, idField).equal(id).ifElse((_$2) => East.value(some(r), OptionType(rowType)), (_$2) => East.value(none, OptionType(rowType))));
|
|
352
|
+
})
|
|
353
|
+
: East.function([StringType, IntegerType], OptionType(rowType), ($, id, offset) => {
|
|
354
|
+
const src = $.const(source.source);
|
|
355
|
+
const noRow = $.const(none, OptionType(rowType));
|
|
356
|
+
return src.page(offset, 1n).match({
|
|
357
|
+
none: (_$) => noRow,
|
|
358
|
+
some: (_$, win) => source.keyed
|
|
359
|
+
? win.tryGet(id)
|
|
360
|
+
: win.firstMap((_$2, r) => idOf(r, idField).equal(id).ifElse((_$3) => East.value(some(r), OptionType(rowType)), (_$3) => East.value(none, OptionType(rowType)))),
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
// The driver lookup — the driver data folded once into a dictionary; a
|
|
364
|
+
// driverless sheet's lookup is always `none` (its `D` is `Null`).
|
|
365
|
+
const lookupDriver = driver !== undefined
|
|
366
|
+
? East.function([StringType], OptionType(driverType), ($, k) => {
|
|
367
|
+
const dict = $.const(driver.data.toDict((_$, d) => driver.keyFn(d), (_$, d) => d), DictType(StringType, driverType));
|
|
368
|
+
return dict.tryGet(k);
|
|
369
|
+
})
|
|
370
|
+
: East.function([StringType], OptionType(driverType), ($, _k) => $.const(none, OptionType(driverType)));
|
|
371
|
+
// The wire context → Sheet.Types.Context(R, D).
|
|
372
|
+
const bridgeCtx = East.function([SheetContextType], ctxType, ($, ctx) => {
|
|
373
|
+
const byId = $.const(rowById);
|
|
374
|
+
const dec = $.const(decode);
|
|
375
|
+
const row = $.let(dec(ctx.rowId, ctx.row, byId(ctx.rowId, ctx.offset)), rowType);
|
|
376
|
+
const rows = $.let(ctx.rows.map((_$, r, i) => dec(r.id, r.cells, byId(r.id, ctx.rowsOffset.add(i)))), ArrayType(rowType));
|
|
377
|
+
const lookup = $.const(lookupDriver);
|
|
378
|
+
const driverRow = $.let(ctx.driver.match({
|
|
379
|
+
none: (_$) => East.value(none, OptionType(driverType)),
|
|
380
|
+
some: (_$, k) => lookup(k),
|
|
381
|
+
}), OptionType(driverType));
|
|
382
|
+
return East.value({
|
|
383
|
+
rowIndex: ctx.rowIndex,
|
|
384
|
+
row,
|
|
385
|
+
rows,
|
|
386
|
+
partial: ctx.partial,
|
|
387
|
+
driver: driverRow,
|
|
388
|
+
today: ctx.today,
|
|
389
|
+
}, ctxType);
|
|
390
|
+
});
|
|
391
|
+
// Sheet.Types.Patch(R) → the set fields as cells.
|
|
392
|
+
const encodePatch = East.function([patchType], SheetCellsType, ($, patch) => {
|
|
393
|
+
const cells = $.let(new Map(), SheetCellsType);
|
|
394
|
+
const p = patch;
|
|
395
|
+
const bound = new Map();
|
|
396
|
+
for (const name of stringFormRegisters) {
|
|
397
|
+
bound.set(name, $.const(registers[name], SheetRegisterMembersType));
|
|
398
|
+
}
|
|
399
|
+
for (const m of metas) {
|
|
400
|
+
if (!m.editable || m.derived !== undefined)
|
|
401
|
+
continue;
|
|
402
|
+
if (m.form === "array") {
|
|
403
|
+
// The other half is `none` when the column names no other field.
|
|
404
|
+
const otherKey = m.otherField;
|
|
405
|
+
const own = $.let(p[m.key], OptionType(SheetMembersType));
|
|
406
|
+
const other = $.let((otherKey !== undefined ? p[otherKey] : none), OptionType(SheetMembersType));
|
|
407
|
+
$.if(own.hasTag("some").or(() => other.hasTag("some")), ($2) => {
|
|
408
|
+
const empty = $2.const([], SheetMembersType);
|
|
409
|
+
const ownVal = $2.let(own.match({ some: (_$, v) => v, none: (_$) => empty }), SheetMembersType);
|
|
410
|
+
const otherVal = $2.let(other.match({ some: (_$, v) => v, none: (_$) => empty }), SheetMembersType);
|
|
411
|
+
const link = m.ownHalf === "from"
|
|
412
|
+
? $2.let({ from: ownVal, to: otherVal }, SheetLinkType)
|
|
413
|
+
: $2.let({ from: otherVal, to: ownVal }, SheetLinkType);
|
|
414
|
+
$2(cells.insert(m.key, cellOfPayload("Link", link)));
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const field = $.let(p[m.key], OptionType(fields[m.key]));
|
|
419
|
+
$.if(field.hasTag("some"), ($2) => {
|
|
420
|
+
const v = $2.let(field.unwrap("some"), fields[m.key]);
|
|
421
|
+
const asRow = { [m.key]: v };
|
|
422
|
+
$2(cells.insert(m.key, projectCell(m, asRow, m.register !== undefined ? bound.get(m.register) : undefined)));
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return cells;
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
rowType, driverType, ctxType, checkCtxType, editType, patchType, proposalType,
|
|
429
|
+
projectRow, decode,
|
|
430
|
+
rowById: rowById,
|
|
431
|
+
bridgeCtx, encodePatch,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
// ============================================================================
|
|
435
|
+
// Wrappers — the author's typed functions inside their wire twins
|
|
436
|
+
// ============================================================================
|
|
437
|
+
/** Whether an East function value is asynchronous. */
|
|
438
|
+
export function isAsyncFunction(fn) {
|
|
439
|
+
return typeOf(fn).type === "AsyncFunction";
|
|
440
|
+
}
|
|
441
|
+
/** Pin an author function value and check its East input type against the bridge's context type. */
|
|
442
|
+
function pinProvider(bridge, fn, where, expectedOutput) {
|
|
443
|
+
const expr = East.value(fn);
|
|
444
|
+
const t = typeOf(expr);
|
|
445
|
+
if (t.type !== "Function" && t.type !== "AsyncFunction") {
|
|
446
|
+
throw new Error(`Sheet: ${where} must be an East.function or East.asyncFunction — got a ${t.type}`);
|
|
447
|
+
}
|
|
448
|
+
const inputs = t.inputs ?? [];
|
|
449
|
+
if (inputs.length !== 1 || inputs[0] !== bridge.ctxType) {
|
|
450
|
+
throw new Error(`Sheet: ${where} must take exactly this sheet's context — Sheet.Types.Context(RowType${bridge.driverType === NullType ? "" : ", DriverType"}) — as its one argument; a function written over another row or driver type cannot run here`);
|
|
451
|
+
}
|
|
452
|
+
if (t.output !== expectedOutput) {
|
|
453
|
+
throw new Error(`Sheet: ${where} returns the wrong type — it must return ${describeType(expectedOutput)}`);
|
|
454
|
+
}
|
|
455
|
+
return { fn: expr, async: t.type === "AsyncFunction" };
|
|
456
|
+
}
|
|
457
|
+
/** A short description of an East type for a refusal message. */
|
|
458
|
+
function describeType(t) {
|
|
459
|
+
const v = t;
|
|
460
|
+
if (v.type === "Variant" && v.cases !== undefined && optionPayload(t) !== undefined)
|
|
461
|
+
return `Option<${describeType(optionPayload(t))}>`;
|
|
462
|
+
if (v.type === "Struct" && v.fields !== undefined)
|
|
463
|
+
return `{ ${Object.entries(v.fields).map(([k, f]) => `${k}: ${describeType(f)}`).join(", ")} }`;
|
|
464
|
+
if (v.type === "Array" && v.value !== undefined)
|
|
465
|
+
return `Array<${describeType(v.value)}>`;
|
|
466
|
+
return v.type;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Wrap a fill provider — `Sheet.Types.Context(R, D)` → `Option<Fill(T)>` —
|
|
470
|
+
* into the wire provider variant (sync or async by the function's type).
|
|
471
|
+
*/
|
|
472
|
+
export function wrapProvider(bridge, meta, fn, index) {
|
|
473
|
+
const fillType = StructType({ value: meta.payloadType, meta: StringType });
|
|
474
|
+
const { fn: author, async } = pinProvider(bridge, fn, `column "${meta.key}" fill provider #${index + 1}`, OptionType(fillType));
|
|
475
|
+
const body = ($, ctx) => {
|
|
476
|
+
const a = $.const(author);
|
|
477
|
+
const bc = $.const(bridge.bridgeCtx);
|
|
478
|
+
const typed = $.let(bc(ctx), bridge.ctxType);
|
|
479
|
+
return a(typed).match({
|
|
480
|
+
none: (_$) => East.value(none, OptionType(SheetFillType)),
|
|
481
|
+
some: (_$, f) => East.value(some({ value: cellOfPayload(meta.cellTag, f.value), meta: f.meta }), OptionType(SheetFillType)),
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
const wire = async
|
|
485
|
+
? East.asyncFunction([SheetContextType], OptionType(SheetFillType), body)
|
|
486
|
+
: East.function([SheetContextType], OptionType(SheetFillType), body);
|
|
487
|
+
return East.value(variant(async ? "async" : "sync", wire), SheetProviderType);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Wrap a row proposer — `Sheet.Types.Context(R, D)` → `Array<Proposal(R)>` —
|
|
491
|
+
* into the wire proposer variant.
|
|
492
|
+
*/
|
|
493
|
+
export function wrapProposer(bridge, fn, index) {
|
|
494
|
+
const { fn: author, async } = pinProvider(bridge, fn, `suggest.propose #${index + 1}`, ArrayType(bridge.proposalType));
|
|
495
|
+
const body = ($, ctx) => {
|
|
496
|
+
const a = $.const(author);
|
|
497
|
+
const bc = $.const(bridge.bridgeCtx);
|
|
498
|
+
const enc = $.const(bridge.encodePatch);
|
|
499
|
+
const typed = $.let(bc(ctx), bridge.ctxType);
|
|
500
|
+
return a(typed).map((_$, p) => East.value({ cells: enc(p.patch), meta: p.meta }, SheetProposalType));
|
|
501
|
+
};
|
|
502
|
+
const wire = async
|
|
503
|
+
? East.asyncFunction([SheetContextType], ArrayType(SheetProposalType), body)
|
|
504
|
+
: East.function([SheetContextType], ArrayType(SheetProposalType), body);
|
|
505
|
+
return East.value(variant(async ? "async" : "sync", wire), SheetProposerType);
|
|
506
|
+
}
|
|
507
|
+
/** Wrap an arity rule — `Sheet.Types.Context(R, D)` → `Option<Counted>`. */
|
|
508
|
+
export function wrapArity(bridge, meta, fn) {
|
|
509
|
+
const { fn: author, async } = pinProvider(bridge, fn, `column "${meta.key}" arity rule`, OptionType(SheetCountedType));
|
|
510
|
+
if (async)
|
|
511
|
+
throw new Error(`Sheet: column "${meta.key}" arity rule must be synchronous — the strip reads it while the half is edited`);
|
|
512
|
+
return East.function([SheetContextType], OptionType(SheetCountedType), ($, ctx) => {
|
|
513
|
+
const a = $.const(author);
|
|
514
|
+
const bc = $.const(bridge.bridgeCtx);
|
|
515
|
+
return a(bc(ctx));
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
/** Wrap an author member check — `Sheet.Types.CheckContext(R)` → `Option<String>` — into the `custom` check arm. */
|
|
519
|
+
export function wrapCheck(bridge, meta, fn, index) {
|
|
520
|
+
const expr = East.value(fn);
|
|
521
|
+
const t = typeOf(expr);
|
|
522
|
+
if (t.type !== "Function" || (t.inputs ?? []).length !== 1 || t.inputs[0] !== bridge.checkCtxType || t.output !== OptionType(StringType)) {
|
|
523
|
+
throw new Error(`Sheet: column "${meta.key}" check #${index + 1} must be an East.function over Sheet.Types.CheckContext(RowType) returning Option<String>`);
|
|
524
|
+
}
|
|
525
|
+
const wire = East.function([SheetCheckContextType], OptionType(StringType), ($, c) => {
|
|
526
|
+
const a = $.const(expr);
|
|
527
|
+
const byId = $.const(bridge.rowById);
|
|
528
|
+
const dec = $.const(bridge.decode);
|
|
529
|
+
const row = $.let(dec(c.rowId, c.row, byId(c.rowId, c.offset)), bridge.rowType);
|
|
530
|
+
return a(East.value({ rowIndex: c.rowIndex, row, half: c.half, member: c.member }, bridge.checkCtxType));
|
|
531
|
+
});
|
|
532
|
+
return East.value(variant("custom", wire), SheetCheckType);
|
|
533
|
+
}
|
|
534
|
+
/** Wrap a custom kind's `parse` — `(text, Context(R, D))` → `Option<P>` — into the wire parse. */
|
|
535
|
+
export function wrapCustomParse(bridge, meta, fn) {
|
|
536
|
+
const expr = East.value(fn);
|
|
537
|
+
const t = typeOf(expr);
|
|
538
|
+
const inputs = t.inputs ?? [];
|
|
539
|
+
if (t.type !== "Function" || inputs.length !== 2 || inputs[0] !== StringType || inputs[1] !== bridge.ctxType) {
|
|
540
|
+
throw new Error(`Sheet: custom column "${meta.key}" \`parse\` must be an East.function over (String, Sheet.Types.Context(RowType, DriverType)) returning Option<payload>`);
|
|
541
|
+
}
|
|
542
|
+
return East.function([StringType, SheetContextType], OptionType(SheetCellType), ($, text, ctx) => {
|
|
543
|
+
const a = $.const(expr);
|
|
544
|
+
const bc = $.const(bridge.bridgeCtx);
|
|
545
|
+
return a(text, bc(ctx)).match({
|
|
546
|
+
none: (_$) => East.value(none, OptionType(SheetCellType)),
|
|
547
|
+
some: (_$, v) => East.value(some(cellOfPayload(meta.cellTag, v)), OptionType(SheetCellType)),
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/** Wrap a custom kind's `print` — `P` → `String` — into the wire print. */
|
|
552
|
+
export function wrapCustomPrint(meta, fn) {
|
|
553
|
+
const expr = East.value(fn);
|
|
554
|
+
const t = typeOf(expr);
|
|
555
|
+
const inputs = t.inputs ?? [];
|
|
556
|
+
if (t.type !== "Function" || inputs.length !== 1 || inputs[0] !== meta.payloadType || t.output !== StringType) {
|
|
557
|
+
throw new Error(`Sheet: custom column "${meta.key}" \`print\` must be an East.function over the payload (${describeType(meta.payloadType)}) returning String`);
|
|
558
|
+
}
|
|
559
|
+
return East.function([SheetCellType], StringType, ($, cell) => {
|
|
560
|
+
const a = $.const(expr);
|
|
561
|
+
return cell.match({ [meta.cellTag]: (_$, v) => a(v) }, (_$) => East.value("", StringType));
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/** Wrap a typed `onEdit` — `Sheet.Types.Edit(R)` → `Null` — into the wire edit handler. */
|
|
565
|
+
export function wrapOnEdit(bridge, fn) {
|
|
566
|
+
const expr = East.value(fn, FunctionType([bridge.editType], NullType));
|
|
567
|
+
return East.function([SheetEditType], NullType, ($, e) => {
|
|
568
|
+
const a = $.const(expr);
|
|
569
|
+
const byId = $.const(bridge.rowById);
|
|
570
|
+
const dec = $.const(bridge.decode);
|
|
571
|
+
const noRow = $.const(none, OptionType(bridge.rowType));
|
|
572
|
+
const et = bridge.editType;
|
|
573
|
+
$(e.match({
|
|
574
|
+
commit: (_$, c) => a(East.value(variant("commit", {
|
|
575
|
+
rowId: c.rowId, key: c.key, row: dec(c.rowId, c.row.cells, byId(c.rowId, c.offset)), source: c.source,
|
|
576
|
+
}), et)),
|
|
577
|
+
insert: (_$, i) => a(East.value(variant("insert", {
|
|
578
|
+
afterRowId: i.afterRowId, row: dec(i.row.id, i.row.cells, noRow), source: i.source,
|
|
579
|
+
}), et)),
|
|
580
|
+
remove: (_$, r) => a(East.value(variant("remove", { rowIds: r.rowIds }), et)),
|
|
581
|
+
}));
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Compile `onUpdate` into the wire edit handler (§4.7) — the whole-value
|
|
586
|
+
* rebuild: a commit rewrites one row, an insert appends a fresh row after
|
|
587
|
+
* `afterRowId`, a remove filters ids; then the author's `onUpdate` receives
|
|
588
|
+
* the collection.
|
|
589
|
+
*/
|
|
590
|
+
export function compileOnUpdate(bridge, rows, idField, fn) {
|
|
591
|
+
const rowsType = ArrayType(bridge.rowType);
|
|
592
|
+
const expr = East.value(fn, FunctionType([rowsType], NullType));
|
|
593
|
+
return East.function([SheetEditType], NullType, ($, ev) => {
|
|
594
|
+
const current = $.let(rows, rowsType);
|
|
595
|
+
const upd = $.const(expr);
|
|
596
|
+
const dec = $.const(bridge.decode);
|
|
597
|
+
const noRow = $.const(none, OptionType(bridge.rowType));
|
|
598
|
+
const next = $.let(ev.match({
|
|
599
|
+
commit: (_$, c) => current.map((_$2, r) => idOf(r, idField).equal(c.rowId).ifElse((_$3) => dec(c.rowId, c.row.cells, East.value(some(r), OptionType(bridge.rowType))), (_$3) => r)),
|
|
600
|
+
insert: ($2, i) => {
|
|
601
|
+
const fresh = $2.let(dec(i.row.id, i.row.cells, noRow), bridge.rowType);
|
|
602
|
+
return i.afterRowId.match({
|
|
603
|
+
none: (_$3) => current.concat(East.value([fresh], rowsType)),
|
|
604
|
+
some: (_$3, after) => current.flatMap((_$4, r) => idOf(r, idField).equal(after).ifElse((_$5) => East.value([r, fresh], rowsType), (_$5) => East.value([r], rowsType))),
|
|
605
|
+
});
|
|
606
|
+
},
|
|
607
|
+
remove: ($2, rm) => {
|
|
608
|
+
const ids = $2.let(rm.rowIds.toSet());
|
|
609
|
+
return current.filter((_$3, r) => ids.has(idOf(r, idField)).not());
|
|
610
|
+
},
|
|
611
|
+
}), rowsType);
|
|
612
|
+
$(upd(next));
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
/** Both channels at once — `onEdit` observes, then `onUpdate` writes. */
|
|
616
|
+
export function composeEditHandlers(observe, write) {
|
|
617
|
+
if (observe === undefined)
|
|
618
|
+
return write;
|
|
619
|
+
if (write === undefined)
|
|
620
|
+
return observe;
|
|
621
|
+
return East.function([SheetEditType], NullType, ($, e) => {
|
|
622
|
+
const o = $.const(observe);
|
|
623
|
+
const w = $.const(write);
|
|
624
|
+
$(o(e));
|
|
625
|
+
$(w(e));
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
/** Re-exported for the root's driver-typed refusals. */
|
|
629
|
+
export { SheetHalfType, BooleanType, AsyncFunctionType, EMPTY_LINK };
|
|
630
|
+
//# sourceMappingURL=bridge.js.map
|