@valancex/common 0.12.0 → 0.14.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +11 -4
  3. package/dist/app.js +177 -32
  4. package/dist/client.d.ts +4 -1
  5. package/dist/client.js +20 -4
  6. package/dist/color-scheme.d.ts +12 -0
  7. package/dist/color-scheme.js +15 -0
  8. package/dist/desktop.d.ts +24 -0
  9. package/dist/desktop.js +72 -0
  10. package/dist/early-replay.d.ts +57 -0
  11. package/dist/early-replay.js +75 -0
  12. package/dist/early.d.ts +8 -0
  13. package/dist/early.js +30 -0
  14. package/dist/form.d.ts +165 -0
  15. package/dist/form.js +388 -0
  16. package/dist/http.d.ts +4 -1
  17. package/dist/http.js +7 -1
  18. package/dist/index.d.ts +167 -4
  19. package/dist/index.js +68 -0
  20. package/dist/internal.d.ts +7 -0
  21. package/dist/internal.js +7 -0
  22. package/dist/navigation.d.ts +6 -0
  23. package/dist/navigation.js +5 -0
  24. package/dist/open-system.d.ts +12 -0
  25. package/dist/open-system.js +31 -0
  26. package/dist/open.d.ts +23 -0
  27. package/dist/open.js +47 -0
  28. package/dist/platform.d.ts +5 -0
  29. package/dist/platform.js +4 -0
  30. package/dist/primitives.d.ts +21 -0
  31. package/dist/primitives.js +792 -31
  32. package/dist/record.d.ts +4 -0
  33. package/dist/record.js +14 -0
  34. package/dist/scope.d.ts +11 -2
  35. package/dist/scope.js +10 -1
  36. package/dist/server.js +4 -2
  37. package/dist/store-file.d.ts +4 -0
  38. package/dist/store-file.js +49 -0
  39. package/dist/store.d.ts +40 -0
  40. package/dist/store.js +60 -0
  41. package/dist/targets.d.ts +8 -0
  42. package/dist/targets.js +22 -0
  43. package/dist/vocabulary.d.ts +47 -2
  44. package/dist/vocabulary.js +24 -2
  45. package/dist/web-app.d.ts +6 -0
  46. package/dist/web-app.js +20 -0
  47. package/package.json +14 -5
package/dist/form.js ADDED
@@ -0,0 +1,388 @@
1
+ // Forms from the schema (direction Bet 11). One description of valid: the schema the submitted command takes. A form reads it once for what a screen needs (a field's name, kind, title, constraints), keeps
2
+ // what the person typed as text, and decides from the same schema what is wrong and when to say so. It is a model, not a primitive: the fields are drawn with the ordinary `textfield`, `checkbox` and
3
+ // `radiogroup`, and the state is the application's own (a field of its state), so the command that submits reads every draft.
4
+ //
5
+ // const Signup = Schema.Struct({ name: Schema.String.pipe(Schema.minLength(2)).annotations({ title: "Name", message: () => "Enter at least 2 characters" }), … });
6
+ // const signup = Form.make(Signup);
7
+ // defineApp({ state: { signup: signup.field() }, derived: (derive) => ({ form: signup.view(derive, (s) => s.signup) }), commands: (command) => signup.commands(command, { … }), … });
8
+ //
9
+ // What it keeps for each field: the text typed (`value`), whether the person has left the field (`touched`), and the message a server gave it (`remote`). `dirty` and the message are worked out. An error is
10
+ // shown for a field when it is wrong and the person has left the field once or tried to submit (stricter than Angular's dirty-or-touched and Phoenix's used-input, so the first keystroke is not an error); once
11
+ // shown it follows the typing. A failed submit touches every field. Tier 1: the defaults are the whole interface; the options (`initial`, `message`, `required`) are Tier 2.
12
+ import { Either, Option, ParseResult, Schema, SchemaAST as AST } from "effect";
13
+ import { FIELD, FORM } from "./index.js";
14
+ /** A mistake in the schema a form is made from, said at the declaration and naming the field. */
15
+ export class FormError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "FormError";
19
+ }
20
+ }
21
+ /** The schema of a form's state: a struct with a named entry for each field (the model has no type for a record with free keys), so it can be a field of the application's state and be read in a template. */
22
+ const stateSchemaOf = (fields) => Schema.Struct({
23
+ values: Schema.Struct(Object.fromEntries(fields.map((each) => [each.name, each.kind === "boolean" ? Schema.Boolean : Schema.String]))),
24
+ touched: Schema.Struct(Object.fromEntries(fields.map((each) => [each.name, Schema.optional(Schema.Literal(true))]))),
25
+ attempts: Schema.Number,
26
+ remote: Schema.Struct(Object.fromEntries(fields.map((each) => [each.name, Schema.optional(Schema.String)]))),
27
+ });
28
+ const FieldViewSchema = Schema.Struct({
29
+ name: Schema.String,
30
+ title: Schema.String,
31
+ kind: Schema.Literal("text", "password", "number", "boolean", "choice"),
32
+ input: Schema.String,
33
+ required: Schema.Boolean,
34
+ choices: Schema.Array(Schema.String),
35
+ value: Schema.String,
36
+ checked: Schema.Boolean,
37
+ invalid: Schema.Boolean,
38
+ error: Schema.String,
39
+ errorId: Schema.String,
40
+ touched: Schema.Boolean,
41
+ dirty: Schema.Boolean,
42
+ });
43
+ /** The type of `View`, for the derived value a template reads. */
44
+ export const ViewSchema = Schema.Struct({
45
+ fields: Schema.Array(FieldViewSchema),
46
+ valid: Schema.Boolean,
47
+ message: Schema.String,
48
+ first: Schema.String,
49
+ submitted: Schema.Boolean,
50
+ });
51
+ export const fieldErrors = (errors) => ({ _tag: "FieldErrors", errors });
52
+ // ---- describing a schema ----------------------------------------------------------------------------------------------------------------------------------------
53
+ const SCHEMA_ID = AST.SchemaIdAnnotationId;
54
+ /** The titles Effect gives a refinement or a keyword by itself: `minLength(2)`, `int`, `string`. An author's title is anything else. */
55
+ const generated = (title) => /^\w+\(.*\)$/.test(title) || ["string", "number", "boolean", "int", "nonEmpty", "trimmed", "lowercased", "uppercased", "finite", "nonNaN"].includes(title);
56
+ const titleOf = (node) => {
57
+ const given = node.annotations[AST.TitleAnnotationId];
58
+ return typeof given === "string" && !generated(given) ? given : undefined;
59
+ };
60
+ const unwrapOptional = (ast) => {
61
+ if (ast._tag !== "Union") {
62
+ return ast;
63
+ }
64
+ const rest = ast.types.filter((each) => each._tag !== "UndefinedKeyword");
65
+ return rest.length === 1 ? rest[0] : ast;
66
+ };
67
+ /** The encoded base of a type (what is typed) and its decoded base, the refinements met on the way down, and the titles. */
68
+ const baseOf = (given, side, refinements = []) => {
69
+ const ast = unwrapOptional(given);
70
+ if (ast._tag === "Refinement") {
71
+ refinements.push(ast);
72
+ return baseOf(ast.from, side, refinements);
73
+ }
74
+ if (ast._tag === "Transformation") {
75
+ return baseOf(side === "encoded" ? ast.from : ast.to, side, refinements);
76
+ }
77
+ if (ast._tag === "Suspend") {
78
+ return baseOf(ast.f(), side, refinements);
79
+ }
80
+ return { base: ast, refinements };
81
+ };
82
+ const literalsOf = (ast) => {
83
+ if (ast._tag === "Literal") {
84
+ return typeof ast.literal === "bigint" || ast.literal === null ? undefined : [ast.literal];
85
+ }
86
+ if (ast._tag === "Union") {
87
+ const each = ast.types.map(literalsOf);
88
+ return each.every((one) => one !== undefined) ? each.flat() : undefined;
89
+ }
90
+ return undefined;
91
+ };
92
+ const describeField = (path, ps) => {
93
+ const name = path.join(".");
94
+ const encoded = baseOf(ps.type, "encoded");
95
+ const decoded = baseOf(ps.type, "decoded");
96
+ const chain = [...encoded.refinements, ...decoded.refinements.filter((each) => !encoded.refinements.includes(each))];
97
+ const title = titleOf(ps) ?? titleOf(ps.type) ?? chain.map(titleOf).find((each) => each !== undefined);
98
+ if (title === undefined) {
99
+ throw new FormError(`Form: the field "${name}" has no title: give the schema \`.annotations({ title: "…" })\`, the label the person reads (and the name a screen reader says)`);
100
+ }
101
+ const optional = ps.isOptional || (ps.type._tag === "Union" && ps.type.types.some((each) => each._tag === "UndefinedKeyword"));
102
+ const constraints = Object.assign({}, ...chain.map((each) => each.annotations[AST.JSONSchemaAnnotationId] ?? {}));
103
+ const hasMessage = chain.some((each) => each.annotations[AST.MessageAnnotationId] !== undefined) || ps.type.annotations[AST.MessageAnnotationId] !== undefined;
104
+ const typed = encoded.base;
105
+ const literals = literalsOf(typed);
106
+ let kind;
107
+ let choices = [];
108
+ let redacted = false;
109
+ if (typed._tag === "StringKeyword" && decoded.base._tag === "Declaration") {
110
+ kind = "password";
111
+ redacted = true;
112
+ }
113
+ else if (typed._tag === "StringKeyword") {
114
+ kind = decoded.base._tag === "NumberKeyword" ? "number" : "text";
115
+ }
116
+ else if (typed._tag === "NumberKeyword") {
117
+ kind = "number";
118
+ }
119
+ else if (typed._tag === "BooleanKeyword" || (literals !== undefined && literals.length === 1 && typeof literals[0] === "boolean")) {
120
+ kind = "boolean";
121
+ }
122
+ else if (literals !== undefined && literals.every((each) => typeof each === "string")) {
123
+ kind = "choice";
124
+ choices = literals;
125
+ }
126
+ else if (typed._tag === "TypeLiteral" || typed._tag === "TupleType") {
127
+ throw new FormError(`Form: the field "${name}" is ${typed._tag === "TupleType" ? "a list" : "a record"} where a value is needed (lists of fields are not supported yet; flat and nested records are)`);
128
+ }
129
+ else {
130
+ throw new FormError(`Form: the field "${name}" has a type a form cannot draw (${typed._tag}): text, a number typed as text, a boolean, a closed set of strings, or a secret`);
131
+ }
132
+ return { name, path, title, kind, required: !optional && kind !== "boolean", choices, constraints, redacted, hasMessage };
133
+ };
134
+ /** The message the author wrote on a field's type (or on the field), by field name: what the field says whichever of its checks fails. */
135
+ const fieldMessagesOf = (ast, path = []) => {
136
+ const found = new Map();
137
+ const record = baseOf(ast, "decoded").base;
138
+ if (record._tag !== "TypeLiteral") {
139
+ return found;
140
+ }
141
+ for (const ps of record.propertySignatures) {
142
+ if (typeof ps.name !== "string") {
143
+ continue;
144
+ }
145
+ const here = [...path, ps.name];
146
+ const inner = baseOf(ps.type, "encoded").base;
147
+ if (inner._tag === "TypeLiteral") {
148
+ fieldMessagesOf(inner, here).forEach((value, key) => found.set(key, value));
149
+ continue;
150
+ }
151
+ const said = ps.annotations[AST.MessageAnnotationId] ?? unwrapOptional(ps.type).annotations[AST.MessageAnnotationId];
152
+ if (typeof said === "function") {
153
+ found.set(here.join("."), said);
154
+ }
155
+ }
156
+ return found;
157
+ };
158
+ const describe = (ast, path = []) => {
159
+ const { base } = baseOf(ast, "decoded");
160
+ const record = base._tag === "TypeLiteral" ? base : baseOf(ast, "encoded").base;
161
+ if (record._tag !== "TypeLiteral") {
162
+ throw new FormError("Form: a form is made from a struct: `Schema.Struct({ … })`");
163
+ }
164
+ return record.propertySignatures.flatMap((ps) => {
165
+ if (typeof ps.name !== "string") {
166
+ return [];
167
+ }
168
+ const here = [...path, ps.name];
169
+ const inner = baseOf(ps.type, "encoded").base;
170
+ return inner._tag === "TypeLiteral" ? describe(inner, here) : [describeField(here, ps)];
171
+ });
172
+ };
173
+ const leaves = (issue, path, spoken = false) => {
174
+ switch (issue._tag) {
175
+ case "Pointer":
176
+ return leaves(issue.issue, [...path, ...(Array.isArray(issue.path) ? issue.path : [issue.path]).map(String)], spoken);
177
+ case "Composite":
178
+ return (Array.isArray(issue.issues) ? issue.issues : [issue.issues]).flatMap((each) => leaves(each, path, spoken));
179
+ case "Refinement":
180
+ // A rule over a whole struct reports at the paths it names (a Pointer inside): those are the leaves, not the rule.
181
+ if (issue.kind === "From") {
182
+ return leaves(issue.issue, path, spoken);
183
+ }
184
+ return issue.issue._tag === "Pointer" || issue.issue._tag === "Composite" ? leaves(issue.issue, path, true) : [{ path, issue, spoken }];
185
+ case "Transformation":
186
+ return leaves(issue.issue, path, spoken);
187
+ default:
188
+ return [{ path, issue, spoken }];
189
+ }
190
+ };
191
+ const codeOf = (issue) => {
192
+ const ast = "ast" in issue ? issue.ast : undefined;
193
+ const id = ast?.annotations[SCHEMA_ID];
194
+ const params = ast?.annotations[AST.JSONSchemaAnnotationId] ?? {};
195
+ return { code: typeof id === "symbol" ? (id.description ?? "").replace(/^effect\/SchemaId\//, "") : issue._tag, params };
196
+ };
197
+ const summary = (descriptor) => {
198
+ const { minLength, maxLength, minimum, maximum } = descriptor.constraints;
199
+ if (descriptor.kind === "choice") {
200
+ return `Choose one of: ${descriptor.choices.join(", ")}`;
201
+ }
202
+ if (minimum !== undefined && maximum !== undefined) {
203
+ return `Enter a number from ${minimum} to ${maximum}`;
204
+ }
205
+ if (minLength !== undefined) {
206
+ return `Enter at least ${minLength} characters`;
207
+ }
208
+ if (maxLength !== undefined) {
209
+ return `Enter at most ${maxLength} characters`;
210
+ }
211
+ return descriptor.kind === "boolean" ? "This must be checked" : "Enter a valid value";
212
+ };
213
+ // ---- making a form ----------------------------------------------------------------------------------------------------------------------------------------------------
214
+ const hash = (text) => {
215
+ let value = 5381;
216
+ for (let at = 0; at < text.length; at += 1) {
217
+ value = ((value << 5) + value + text.charCodeAt(at)) >>> 0;
218
+ }
219
+ return value.toString(36);
220
+ };
221
+ const setIn = (target, path, value) => {
222
+ let at = target;
223
+ for (const step of path.slice(0, -1)) {
224
+ at = (at[step] ??= {});
225
+ }
226
+ at[path[path.length - 1]] = value;
227
+ };
228
+ export function make(schema, options = {}) {
229
+ const fields = describe(schema.ast);
230
+ const byName = new Map(fields.map((each) => [each.name, each]));
231
+ const fieldMessages = fieldMessagesOf(schema.ast);
232
+ const warnings = fields
233
+ .filter((each) => !each.hasMessage && Object.keys(each.constraints).length > 0)
234
+ .map((each) => `${each.name}: its constraints (${Object.keys(each.constraints).join(", ")}) have no message of their own: add \`message\` to the schema, or a \`message\` function to the form's options`);
235
+ const stateSchema = stateSchemaOf(fields);
236
+ const given = (options.initial ?? {});
237
+ const valueOf = (descriptor) => {
238
+ const at = descriptor.path.reduce((value, step) => (typeof value === "object" && value !== null ? value[step] : undefined), given);
239
+ return typeof at === "boolean" ? at : at === undefined ? (descriptor.kind === "boolean" ? false : "") : String(at);
240
+ };
241
+ const initial = { values: Object.fromEntries(fields.map((each) => [each.name, valueOf(each)])), touched: {}, attempts: 0, remote: {} };
242
+ const version = hash(fields.map((each) => `${each.name}:${each.kind}`).join("|"));
243
+ const known = (name) => {
244
+ const found = byName.get(name);
245
+ if (found === undefined) {
246
+ throw new FormError(`Form: there is no field "${name}" (the fields: ${fields.map((each) => each.name).join(", ")})`);
247
+ }
248
+ return found;
249
+ };
250
+ /** What the schema says is wrong with what is typed: a message for each field (and `""` for the form as a whole), the first the schema gives. */
251
+ const problems = (state) => {
252
+ const encoded = {};
253
+ for (const each of fields) {
254
+ const value = state.values[each.name];
255
+ if (typeof value === "string" && value === "" && !each.required) {
256
+ continue;
257
+ }
258
+ if (each.kind === "choice" && value === "") {
259
+ continue;
260
+ }
261
+ setIn(encoded, each.path, value);
262
+ }
263
+ const result = Schema.decodeUnknownEither(schema, { errors: "all", onExcessProperty: "ignore" })(encoded);
264
+ if (Either.isRight(result)) {
265
+ return { value: result.right, errors: new Map() };
266
+ }
267
+ const errors = new Map();
268
+ for (const { path, issue, spoken } of leaves(result.left.issue, [])) {
269
+ const name = path.join(".");
270
+ if (errors.has(name)) {
271
+ continue;
272
+ }
273
+ const descriptor = byName.get(name);
274
+ const { code, params } = codeOf(issue);
275
+ const own = ("ast" in issue && issue.ast.annotations[AST.MessageAnnotationId] !== undefined) ||
276
+ (spoken && "message" in issue && (typeof issue.message === "string" || (Option.isOption(issue.message) && Option.isSome(issue.message))));
277
+ const ofField = fieldMessages.get(name);
278
+ const said = own || ofField === undefined || issue._tag === "Missing" ? undefined : ofField(issue);
279
+ const fromField = typeof said === "string" ? said : typeof said === "object" && said !== null && "message" in said ? String(said.message) : undefined;
280
+ // The author's message on the check, else on the field (it speaks for every check of the field), else "Required", the application's words, the schema's own words.
281
+ const message = (own ? ParseResult.TreeFormatter.formatIssueSync(issue) : undefined) ??
282
+ fromField ??
283
+ (issue._tag === "Missing" ? "Required" : undefined) ??
284
+ options.message?.({ path, code, params, actual: descriptor?.redacted === true ? undefined : issue.actual }) ??
285
+ (descriptor === undefined ? ParseResult.TreeFormatter.formatIssueSync(issue) : summary(descriptor));
286
+ errors.set(name, message);
287
+ }
288
+ return { errors };
289
+ };
290
+ const touchedAll = (state) => ({ ...state, attempts: state.attempts + 1, touched: Object.fromEntries(fields.map((each) => [each.name, true])) });
291
+ const view = ((state, from) => {
292
+ if (typeof state === "function") {
293
+ return state(ViewSchema, (application) => view(from(application)));
294
+ }
295
+ const found = problems(state).errors;
296
+ const shown = (name) => state.remote[name] ?? (state.touched[name] === true || state.attempts > 0 ? (found.get(name) ?? "") : "");
297
+ const views = fields.map((each) => {
298
+ const value = state.values[each.name];
299
+ const error = shown(each.name);
300
+ return {
301
+ name: each.name,
302
+ title: each.title,
303
+ kind: each.kind,
304
+ input: each.kind === "password" ? "password" : "text",
305
+ required: each.required,
306
+ choices: each.choices,
307
+ value: typeof value === "string" ? value : "",
308
+ checked: value === true,
309
+ invalid: error !== "",
310
+ error,
311
+ errorId: `${each.name.replace(/\./g, "-")}-error`,
312
+ touched: state.touched[each.name] === true,
313
+ dirty: value !== initial.values[each.name],
314
+ };
315
+ });
316
+ const count = views.filter((each) => each.invalid).length;
317
+ return {
318
+ fields: views,
319
+ valid: found.size === 0 && Object.keys(state.remote).length === 0,
320
+ message: state.attempts > 0 && count > 0 ? `${count} ${count === 1 ? "field needs" : "fields need"} attention` : "",
321
+ first: views.find((each) => each.invalid)?.name ?? "",
322
+ submitted: state.attempts > 0,
323
+ };
324
+ });
325
+ const without = (record, name) => Object.fromEntries(Object.entries(record).filter(([key]) => key !== name));
326
+ const form = {
327
+ fields,
328
+ initial,
329
+ standard: Schema.standardSchemaV1(schema),
330
+ warnings,
331
+ field: () => ({ [FIELD]: true, [FORM]: true, schema: stateSchema, initial }),
332
+ view,
333
+ edit: (state, name, value) => {
334
+ known(name);
335
+ return { ...state, values: { ...state.values, [name]: value }, remote: without(state.remote, name) };
336
+ },
337
+ check: (state, name, value) => {
338
+ known(name);
339
+ return { ...state, values: { ...state.values, [name]: value }, touched: { ...state.touched, [name]: true }, remote: without(state.remote, name) };
340
+ },
341
+ blur: (state, name) => {
342
+ known(name);
343
+ return { ...state, touched: { ...state.touched, [name]: true } };
344
+ },
345
+ submit: (state) => {
346
+ const attempted = touchedAll(state);
347
+ const { value, errors } = problems(attempted);
348
+ return value !== undefined && Object.keys(state.remote).length === 0 && errors.size === 0 ? { ok: true, value, state: attempted } : { ok: false, state: attempted };
349
+ },
350
+ reject: (state, { errors }) => {
351
+ for (const name of Object.keys(errors)) {
352
+ known(name);
353
+ }
354
+ return {
355
+ ...state,
356
+ attempts: state.attempts + 1,
357
+ touched: { ...state.touched, ...Object.fromEntries(Object.keys(errors).map((name) => [name, true])) },
358
+ remote: { ...state.remote, ...errors },
359
+ };
360
+ },
361
+ draft: (state) => ({ version, values: Object.fromEntries(fields.filter((each) => !each.redacted).map((each) => [each.name, state.values[each.name]])) }),
362
+ restore: (draft) => draft.version !== version
363
+ ? initial
364
+ : { ...initial, values: { ...initial.values, ...Object.fromEntries(Object.entries(draft.values).filter(([name]) => byName.has(name) && !byName.get(name).redacted)) } },
365
+ commands: (command, given) => {
366
+ const field = given.field;
367
+ const how = {
368
+ get: given.get ?? ((state) => state[field]),
369
+ set: given.set ?? ((state, form) => ({ ...state, [field]: form })),
370
+ submitted: given.submitted,
371
+ };
372
+ if (given.field === undefined && (given.get === undefined || given.set === undefined)) {
373
+ throw new FormError('Form: say where the form is: `field: "signup"`, or `get` and `set`');
374
+ }
375
+ return {
376
+ edit: command(Schema.Struct({ name: Schema.String, value: Schema.String }), ({ name, value }, current) => how.set(current, form.edit(how.get(current), name, value))),
377
+ check: command(Schema.Struct({ name: Schema.String, value: Schema.Boolean }), ({ name, value }, current) => how.set(current, form.check(how.get(current), name, value))),
378
+ blur: command(Schema.Struct({ name: Schema.String }), ({ name }, current) => how.set(current, form.blur(how.get(current), name))),
379
+ submit: command((current) => {
380
+ const result = form.submit(how.get(current));
381
+ const next = how.set(current, result.state);
382
+ return (result.ok ? how.submitted(next, result.value) : next);
383
+ }),
384
+ };
385
+ },
386
+ };
387
+ return form;
388
+ }
package/dist/http.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as Nexus from "@valancex/nexus";
2
2
  import { Effect, Schema } from "effect";
3
+ import type { StoreClient } from "./store.js";
3
4
  import type { HttpError, Interceptor, Method, TransportRequest } from "./transport.js";
4
5
  export type { HttpError, Interceptor, Method, Transport, TransportRequest, TransportResponse } from "./transport.js";
5
6
  /** One request: where, what is sent, and what the answer must be. */
@@ -62,4 +63,6 @@ export declare const refreshOn401: (refresh: () => Promise<void>) => Interceptor
62
63
  export declare const retryGet: (times: number) => Interceptor;
63
64
  export declare const webHttp: (options: WebHttpOptions) => HttpClient;
64
65
  /** The platform for a web page: the `Http` capability, made from `api.base`. Merged with the application's own by `Web.run`. */
65
- export declare const webPlatform: (options: WebHttpOptions) => Nexus.Application.Platform;
66
+ export declare const webPlatform: (options: WebHttpOptions & {
67
+ readonly store?: StoreClient;
68
+ }) => Nexus.Application.Platform;
package/dist/http.js CHANGED
@@ -13,6 +13,8 @@
13
13
  // bounded in time and size, and its response is *decoded by a Schema* before it can reach state. A failure says what kind it was and never what it carried.
14
14
  import * as Nexus from "@valancex/nexus";
15
15
  import { Effect, FiberRef, Schema } from "effect";
16
+ import { Open, webOpen } from "./open.js";
17
+ import { Store } from "./store.js";
16
18
  /** The capability: ask for it in a command (`Http.request`), and the platform says whether it has one. */
17
19
  export const Http = Nexus.Capability.define("valance/http");
18
20
  /** Makes a request through the platform's client. Requires the `Http` capability. */
@@ -185,4 +187,8 @@ export const webHttp = (options) => {
185
187
  };
186
188
  };
187
189
  /** The platform for a web page: the `Http` capability, made from `api.base`. Merged with the application's own by `Web.run`. */
188
- export const webPlatform = (options) => Nexus.Capability.EnvironmentLive(new Map([[Http.id, { _tag: "Available", implementation: webHttp(options) }]]));
190
+ export const webPlatform = (options) => Nexus.Capability.EnvironmentLive(new Map([
191
+ [Http.id, { _tag: "Available", implementation: webHttp(options) }],
192
+ [Open.id, { _tag: "Available", implementation: webOpen() }],
193
+ ...(options.store === undefined ? [] : [[Store.id, { _tag: "Available", implementation: options.store }]]),
194
+ ]));