@fungi.computer/gills 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +100 -2
- package/dist/declaration.d.ts +55 -0
- package/dist/declaration.js +41 -0
- package/dist/gill.d.ts +248 -0
- package/dist/gill.js +134 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/presentation.d.ts +2 -0
- package/dist/presentation.js +20 -0
- package/dist/rows.d.ts +52 -0
- package/dist/rows.js +37 -0
- package/docs/api/README.md +13 -0
- package/docs/api/index.md +879 -0
- package/docs/api/modules.md +14 -0
- package/docs/api/presentation.md +30 -0
- package/package.json +30 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jamstack Consulting LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Gills (`@fungi.computer/gills`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Gills defines compact, plain-text App views shared by browser and terminal
|
|
4
|
+
hosts. It owns the row wire format, Gill declarations, bounded requests and
|
|
5
|
+
responses, and an authenticated-fetch client. Your host owns admission,
|
|
6
|
+
navigation and read-method admission.
|
|
7
|
+
|
|
8
|
+
## Install or set up
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @fungi.computer/gills effect@4.0.2
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Gills is ESM. Effect is its only runtime dependency. All exports are available
|
|
15
|
+
from `@fungi.computer/gills` in Node, browsers and Workers.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
Decode a row page and the selectors a caller may send:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import * as Schema from "effect/Schema";
|
|
23
|
+
import { rowPageSchemaV1, gillRequestSchema } from "@fungi.computer/gills";
|
|
24
|
+
|
|
25
|
+
const rows = Schema.decodeUnknownSync(rowPageSchemaV1)([
|
|
26
|
+
{ id: "session-a", title: "Review the draft", tone: "attention" },
|
|
27
|
+
]);
|
|
28
|
+
const request = Schema.decodeUnknownSync(gillRequestSchema, {
|
|
29
|
+
onExcessProperty: "error",
|
|
30
|
+
})({ viewId: "inbox" });
|
|
31
|
+
console.log(rows[0]?.title, request.viewId);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core concepts
|
|
35
|
+
|
|
36
|
+
- A **declaration** records the Gill's App-local identity, source read method,
|
|
37
|
+
optional placement parameters and their options read methods, change feed and
|
|
38
|
+
desktop sizes. Agar generates it from the backend; Puffball includes it in the
|
|
39
|
+
Release digest.
|
|
40
|
+
- A **request** contains a view ID, placement parameters and optional source
|
|
41
|
+
cursor. The host admits the method and Computer before invoking the App.
|
|
42
|
+
- A **response** is an empty page, a full page of text rows, or an error. A
|
|
43
|
+
request with `options: <parameter key>` returns a choice page instead.
|
|
44
|
+
`decodeDeclaredGillResponse` rejects another view's identity.
|
|
45
|
+
- A **row** carries display text and optional navigation parameters. The host
|
|
46
|
+
opens the Gill's own App; that App interprets the parameters. Rows cannot name
|
|
47
|
+
a different App, a URL, or a Session-specific target. Navigation values retain
|
|
48
|
+
up to 4,096 characters, including a Computer file path; display shortening
|
|
49
|
+
never changes navigation values.
|
|
50
|
+
- Optional `row.status` carries the App's plain label and semantic tone for a
|
|
51
|
+
compact status indicator. The App maps its state; the host does not infer it.
|
|
52
|
+
- An error can offer `recovery: { kind: "configure", param: "computer" }` or
|
|
53
|
+
`recovery: { kind: "open" }`. Configure must name a declared parameter and
|
|
54
|
+
opens that placement's existing picker. Open uses the host's launcher for the
|
|
55
|
+
same App. Neither form contains an App ID, URL or command. App names and icons
|
|
56
|
+
remain host directory presentation, outside this wire.
|
|
57
|
+
|
|
58
|
+
## API reference
|
|
59
|
+
|
|
60
|
+
See the [generated API](docs/api/index.md) for the schemas, inferred types and
|
|
61
|
+
`createGillClient`.
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
`createGillClient({ endpoint, fetch, declaration })` sends bounded selectors
|
|
66
|
+
with PUT and decodes the result. Supply an authenticated `fetch` pinned to the
|
|
67
|
+
selected App and Release. An optional `decodeRefusal(raw, declaration)` handles
|
|
68
|
+
recognized HTTP 503 bodies using the host's own decoder. Unrecognized refusals
|
|
69
|
+
throw; the client does not choose host policy.
|
|
70
|
+
|
|
71
|
+
Agar's App execution refusal decoder remains in `@fungi.computer/agar/gills`.
|
|
72
|
+
App producers and renderers import the shared contract directly from this
|
|
73
|
+
package.
|
|
74
|
+
|
|
75
|
+
## Limits and errors
|
|
76
|
+
|
|
77
|
+
Pages contain at most 200 rows. Row labels allow 4,096 characters, identities
|
|
78
|
+
513 and details 1,024. A navigation intent has at most 16 parameters, with
|
|
79
|
+
64-character keys and 4,096-character values. Requests bound cursors to 1,024
|
|
80
|
+
characters and placement context to four keys with 256-character values.
|
|
81
|
+
Declarations offer up to four unique parameters of kind Computer, repository or
|
|
82
|
+
branch, and unique desktop sizes from S, M and L. An optional watch names an
|
|
83
|
+
existing storage marker or binding change feed. Resource envelopes and row
|
|
84
|
+
actions are not part of the contract.
|
|
85
|
+
|
|
86
|
+
Decoders reject invalid wire values. Gill output is text data, never executable
|
|
87
|
+
UI. Loading is host state. The client throws on HTTP errors unless the injected
|
|
88
|
+
host decoder recognizes the refusal.
|
|
89
|
+
|
|
90
|
+
## Contribute and license
|
|
91
|
+
|
|
92
|
+
Run `pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm test`, `pnpm docs` and
|
|
93
|
+
`pnpm stranger` in this package. Regenerate API docs with `pnpm docs:write`.
|
|
94
|
+
Licensed under [MIT](LICENSE).
|
|
95
|
+
|
|
96
|
+
### Shared row time copy
|
|
97
|
+
|
|
98
|
+
`relativeTime` from `@fungi.computer/gills/presentation` formats a reported
|
|
99
|
+
millisecond timestamp using the Apps' existing English relative-time copy. This
|
|
100
|
+
pure entry does not load the Gill decoder. Source methods keep their timestamps
|
|
101
|
+
unchanged; a projection only formats timestamps its source returns.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/** Sizes the App explicitly offers for a desktop Gill. */
|
|
3
|
+
export type AppGillDesktopSize = "S" | "M" | "L";
|
|
4
|
+
/** One App-owned placement selector. */
|
|
5
|
+
export type AppGillParam = Readonly<{
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
kind: "computer" | "repository" | "branch";
|
|
9
|
+
required: boolean;
|
|
10
|
+
/** This App's declared read method returning picker choices; Computers are host-owned. */
|
|
11
|
+
options?: string | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
/** An existing change feed used to refresh a Gill. */
|
|
14
|
+
export type AppGillWatch = Readonly<{
|
|
15
|
+
storage: string;
|
|
16
|
+
} | {
|
|
17
|
+
binding: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** A digest-covered projection of one declared read method. */
|
|
20
|
+
export type AppDeclaredGill = Readonly<{
|
|
21
|
+
id: string;
|
|
22
|
+
source: string;
|
|
23
|
+
title: string;
|
|
24
|
+
params?: readonly AppGillParam[] | undefined;
|
|
25
|
+
watch?: AppGillWatch | undefined;
|
|
26
|
+
desktop?: Readonly<{
|
|
27
|
+
sizes: readonly AppGillDesktopSize[];
|
|
28
|
+
}> | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
/** Bounded, App-owned parameter key. */
|
|
31
|
+
export declare const appGillParamKeySchema: Schema.String;
|
|
32
|
+
/** Placement parameter metadata, included in the Release digest. */
|
|
33
|
+
export declare const appGillParamSchema: Schema.Struct<{
|
|
34
|
+
readonly key: Schema.String;
|
|
35
|
+
readonly label: Schema.String;
|
|
36
|
+
readonly kind: Schema.Literals<readonly ["computer", "repository", "branch"]>;
|
|
37
|
+
readonly required: Schema.Boolean;
|
|
38
|
+
readonly options: Schema.optional<Schema.String>;
|
|
39
|
+
}>;
|
|
40
|
+
/** The bounded, uniquely keyed placement parameters shared with desktop catalogs. */
|
|
41
|
+
export declare const appGillParamsSchema: Schema.$Array<Schema.Struct<{
|
|
42
|
+
readonly key: Schema.String;
|
|
43
|
+
readonly label: Schema.String;
|
|
44
|
+
readonly kind: Schema.Literals<readonly ["computer", "repository", "branch"]>;
|
|
45
|
+
readonly required: Schema.Boolean;
|
|
46
|
+
readonly options: Schema.optional<Schema.String>;
|
|
47
|
+
}>>;
|
|
48
|
+
/** A storage marker or host binding's existing change feed. */
|
|
49
|
+
export declare const appGillWatchSchema: Schema.Union<readonly [Schema.Struct<{
|
|
50
|
+
readonly storage: Schema.String;
|
|
51
|
+
}>, Schema.Struct<{
|
|
52
|
+
readonly binding: Schema.String;
|
|
53
|
+
}>]>;
|
|
54
|
+
/** Digest-covered identity, source, context and desktop sizes of a Gill. */
|
|
55
|
+
export declare const appDeclaredGillSchema: Schema.Codec<AppDeclaredGill>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
const isUnique = (values) => new Set(values).size === values.length;
|
|
3
|
+
const meaningful = Schema.String.pipe(Schema.check(Schema.isMaxLength(256)), Schema.check(Schema.makeFilter((value) => value.trim().length > 0, {
|
|
4
|
+
message: "Declared command text must not be blank",
|
|
5
|
+
})));
|
|
6
|
+
/** Bounded, App-owned parameter key. */
|
|
7
|
+
export const appGillParamKeySchema = meaningful.pipe(Schema.check(Schema.isMaxLength(64)));
|
|
8
|
+
/** Placement parameter metadata, included in the Release digest. */
|
|
9
|
+
export const appGillParamSchema = Schema.Struct({
|
|
10
|
+
key: appGillParamKeySchema,
|
|
11
|
+
label: meaningful,
|
|
12
|
+
kind: Schema.Literals(["computer", "repository", "branch"]),
|
|
13
|
+
required: Schema.Boolean,
|
|
14
|
+
options: Schema.optional(meaningful),
|
|
15
|
+
}).check(Schema.makeFilter((param) => param.kind !== "computer" || param.options === undefined, {
|
|
16
|
+
message: "Computer options are host-owned",
|
|
17
|
+
}));
|
|
18
|
+
/** The bounded, uniquely keyed placement parameters shared with desktop catalogs. */
|
|
19
|
+
export const appGillParamsSchema = Schema.Array(appGillParamSchema).check(Schema.isMaxLength(4), Schema.makeFilter((params) => isUnique(params.map((param) => param.key)), {
|
|
20
|
+
message: "Gill parameter keys must be unique",
|
|
21
|
+
}));
|
|
22
|
+
/** A storage marker or host binding's existing change feed. */
|
|
23
|
+
export const appGillWatchSchema = Schema.Union([
|
|
24
|
+
Schema.Struct({ storage: meaningful }),
|
|
25
|
+
Schema.Struct({ binding: meaningful }),
|
|
26
|
+
]);
|
|
27
|
+
/** Digest-covered identity, source, context and desktop sizes of a Gill. */
|
|
28
|
+
export const appDeclaredGillSchema = Schema.Struct({
|
|
29
|
+
id: meaningful.pipe(Schema.check(Schema.makeFilter((id) => !id.includes(":"), {
|
|
30
|
+
message: "Gill IDs are App-local",
|
|
31
|
+
}))),
|
|
32
|
+
title: meaningful,
|
|
33
|
+
source: meaningful,
|
|
34
|
+
params: Schema.optional(appGillParamsSchema),
|
|
35
|
+
watch: Schema.optional(appGillWatchSchema),
|
|
36
|
+
desktop: Schema.optional(Schema.Struct({
|
|
37
|
+
sizes: Schema.Array(Schema.Literals(["S", "M", "L"])).check(Schema.isMinLength(1), Schema.isMaxLength(3), Schema.makeFilter(isUnique, {
|
|
38
|
+
message: "Desktop Gill sizes must be unique",
|
|
39
|
+
})),
|
|
40
|
+
})),
|
|
41
|
+
});
|
package/dist/gill.d.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { type AppDeclaredGill } from "./declaration.js";
|
|
3
|
+
/** Placement values are bounded; the admitting host checks declared keys. */
|
|
4
|
+
export declare const gillParamsSchema: Schema.$Record<Schema.String, Schema.String>;
|
|
5
|
+
/** The caller supplies placement context and a source-owned continuation. */
|
|
6
|
+
export declare const gillRequestSchema: Schema.Struct<{
|
|
7
|
+
readonly viewId: Schema.String;
|
|
8
|
+
readonly params: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
9
|
+
readonly after: Schema.optional<Schema.String>;
|
|
10
|
+
readonly options: Schema.optional<Schema.String>;
|
|
11
|
+
}>;
|
|
12
|
+
/** Selectors supplied by a Gill caller. */
|
|
13
|
+
export type GillRequest = Schema.Schema.Type<typeof gillRequestSchema>;
|
|
14
|
+
/** Runtime call selected by the host after admitting a declared source. */
|
|
15
|
+
export declare const gillCallSchema: Schema.Struct<{
|
|
16
|
+
readonly id: Schema.String;
|
|
17
|
+
readonly params: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
18
|
+
readonly after: Schema.optional<Schema.String>;
|
|
19
|
+
readonly options: Schema.optional<Schema.String>;
|
|
20
|
+
}>;
|
|
21
|
+
/** Input to the generated backend Gill entry. */
|
|
22
|
+
export type GillCall = Schema.Schema.Type<typeof gillCallSchema>;
|
|
23
|
+
/** Shared response codec. Rows are plain text and cannot carry executable UI. */
|
|
24
|
+
export declare const gillResponseSchema: Schema.Union<readonly [Schema.Struct<{
|
|
25
|
+
readonly viewId: Schema.String;
|
|
26
|
+
readonly title: Schema.String;
|
|
27
|
+
readonly state: Schema.Literal<"empty">;
|
|
28
|
+
readonly rows: Schema.Tuple<readonly []>;
|
|
29
|
+
readonly next: Schema.optional<Schema.String>;
|
|
30
|
+
}>, Schema.Struct<{
|
|
31
|
+
readonly viewId: Schema.String;
|
|
32
|
+
readonly title: Schema.String;
|
|
33
|
+
readonly state: Schema.Literal<"error">;
|
|
34
|
+
readonly rows: Schema.Tuple<readonly []>;
|
|
35
|
+
/** Host-owned permanent execution refusal; an ordinary error may still be refreshed. */
|
|
36
|
+
readonly reason: Schema.optional<Schema.Literals<readonly ["exec_isolation_required", "params_required"]>>;
|
|
37
|
+
readonly error: Schema.String;
|
|
38
|
+
/** The host supplies the same-placement picker and this App's launch. */
|
|
39
|
+
readonly recovery: Schema.optional<Schema.Union<readonly [Schema.Struct<{
|
|
40
|
+
readonly kind: Schema.Literal<"configure">;
|
|
41
|
+
readonly param: Schema.String;
|
|
42
|
+
}>, Schema.Struct<{
|
|
43
|
+
readonly kind: Schema.Literal<"open">;
|
|
44
|
+
}>]>>;
|
|
45
|
+
}>, Schema.Struct<{
|
|
46
|
+
readonly viewId: Schema.String;
|
|
47
|
+
readonly title: Schema.String;
|
|
48
|
+
readonly state: Schema.Literal<"full">;
|
|
49
|
+
readonly rows: Schema.$Array<Schema.Struct<{
|
|
50
|
+
readonly id: Schema.String;
|
|
51
|
+
readonly title: Schema.String;
|
|
52
|
+
readonly detail: Schema.optional<Schema.String>;
|
|
53
|
+
readonly tone: Schema.optional<Schema.Literals<readonly ["attention", "running", "error", "settled", "muted"]>>;
|
|
54
|
+
readonly status: Schema.optional<Schema.Struct<{
|
|
55
|
+
readonly label: Schema.String;
|
|
56
|
+
readonly tone: Schema.Literals<readonly ["neutral", "primary", "secondary", "accent", "success", "info", "warning", "error"]>;
|
|
57
|
+
}>>;
|
|
58
|
+
readonly open: Schema.optional<Schema.Struct<{
|
|
59
|
+
readonly params: Schema.$Array<Schema.Struct<{
|
|
60
|
+
readonly key: Schema.String;
|
|
61
|
+
readonly value: Schema.String;
|
|
62
|
+
}>>;
|
|
63
|
+
}>>;
|
|
64
|
+
}>>;
|
|
65
|
+
readonly next: Schema.optional<Schema.String>;
|
|
66
|
+
}>]>;
|
|
67
|
+
/** A decoded page of plain-text Gill rows or a bounded error. */
|
|
68
|
+
export type GillResponse = Schema.Schema.Type<typeof gillResponseSchema>;
|
|
69
|
+
/** Decode backend output once at the App host boundary. */
|
|
70
|
+
export declare const decodeGillResponse: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
71
|
+
readonly state: "empty";
|
|
72
|
+
readonly title: string;
|
|
73
|
+
readonly viewId: string;
|
|
74
|
+
readonly rows: readonly [];
|
|
75
|
+
readonly next?: string | undefined;
|
|
76
|
+
} | {
|
|
77
|
+
readonly error: string;
|
|
78
|
+
readonly state: "error";
|
|
79
|
+
readonly title: string;
|
|
80
|
+
readonly viewId: string;
|
|
81
|
+
readonly rows: readonly [];
|
|
82
|
+
readonly reason?: "exec_isolation_required" | "params_required" | undefined;
|
|
83
|
+
readonly recovery?: {
|
|
84
|
+
readonly kind: "configure";
|
|
85
|
+
readonly param: string;
|
|
86
|
+
} | {
|
|
87
|
+
readonly kind: "open";
|
|
88
|
+
} | undefined;
|
|
89
|
+
} | {
|
|
90
|
+
readonly state: "full";
|
|
91
|
+
readonly title: string;
|
|
92
|
+
readonly viewId: string;
|
|
93
|
+
readonly rows: readonly {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly title: string;
|
|
96
|
+
readonly detail?: string | undefined;
|
|
97
|
+
readonly tone?: "error" | "running" | "attention" | "settled" | "muted" | undefined;
|
|
98
|
+
readonly open?: {
|
|
99
|
+
readonly params: readonly {
|
|
100
|
+
readonly key: string;
|
|
101
|
+
readonly value: string;
|
|
102
|
+
}[];
|
|
103
|
+
} | undefined;
|
|
104
|
+
readonly status?: {
|
|
105
|
+
readonly label: string;
|
|
106
|
+
readonly tone: "accent" | "error" | "success" | "neutral" | "primary" | "secondary" | "info" | "warning";
|
|
107
|
+
} | undefined;
|
|
108
|
+
}[];
|
|
109
|
+
readonly next?: string | undefined;
|
|
110
|
+
};
|
|
111
|
+
/** A bounded choice page returned by an App-owned options read method. */
|
|
112
|
+
export declare const gillOptionsResponseSchema: Schema.Struct<{
|
|
113
|
+
readonly options: Schema.$Array<Schema.Struct<{
|
|
114
|
+
readonly value: Schema.String;
|
|
115
|
+
readonly label: Schema.String;
|
|
116
|
+
readonly detail: Schema.optional<Schema.String>;
|
|
117
|
+
}>>;
|
|
118
|
+
readonly next: Schema.optional<Schema.String>;
|
|
119
|
+
}>;
|
|
120
|
+
/** Choices remain within the selected App; the host supplies Computer choices. */
|
|
121
|
+
export type GillOptionsResponse = Schema.Schema.Type<typeof gillOptionsResponseSchema>;
|
|
122
|
+
/** Shared wire for a rows read or a placement-options read. */
|
|
123
|
+
export declare const gillReplySchema: Schema.Union<readonly [Schema.Union<readonly [Schema.Struct<{
|
|
124
|
+
readonly viewId: Schema.String;
|
|
125
|
+
readonly title: Schema.String;
|
|
126
|
+
readonly state: Schema.Literal<"empty">;
|
|
127
|
+
readonly rows: Schema.Tuple<readonly []>;
|
|
128
|
+
readonly next: Schema.optional<Schema.String>;
|
|
129
|
+
}>, Schema.Struct<{
|
|
130
|
+
readonly viewId: Schema.String;
|
|
131
|
+
readonly title: Schema.String;
|
|
132
|
+
readonly state: Schema.Literal<"error">;
|
|
133
|
+
readonly rows: Schema.Tuple<readonly []>;
|
|
134
|
+
/** Host-owned permanent execution refusal; an ordinary error may still be refreshed. */
|
|
135
|
+
readonly reason: Schema.optional<Schema.Literals<readonly ["exec_isolation_required", "params_required"]>>;
|
|
136
|
+
readonly error: Schema.String;
|
|
137
|
+
/** The host supplies the same-placement picker and this App's launch. */
|
|
138
|
+
readonly recovery: Schema.optional<Schema.Union<readonly [Schema.Struct<{
|
|
139
|
+
readonly kind: Schema.Literal<"configure">;
|
|
140
|
+
readonly param: Schema.String;
|
|
141
|
+
}>, Schema.Struct<{
|
|
142
|
+
readonly kind: Schema.Literal<"open">;
|
|
143
|
+
}>]>>;
|
|
144
|
+
}>, Schema.Struct<{
|
|
145
|
+
readonly viewId: Schema.String;
|
|
146
|
+
readonly title: Schema.String;
|
|
147
|
+
readonly state: Schema.Literal<"full">;
|
|
148
|
+
readonly rows: Schema.$Array<Schema.Struct<{
|
|
149
|
+
readonly id: Schema.String;
|
|
150
|
+
readonly title: Schema.String;
|
|
151
|
+
readonly detail: Schema.optional<Schema.String>;
|
|
152
|
+
readonly tone: Schema.optional<Schema.Literals<readonly ["attention", "running", "error", "settled", "muted"]>>;
|
|
153
|
+
readonly status: Schema.optional<Schema.Struct<{
|
|
154
|
+
readonly label: Schema.String;
|
|
155
|
+
readonly tone: Schema.Literals<readonly ["neutral", "primary", "secondary", "accent", "success", "info", "warning", "error"]>;
|
|
156
|
+
}>>;
|
|
157
|
+
readonly open: Schema.optional<Schema.Struct<{
|
|
158
|
+
readonly params: Schema.$Array<Schema.Struct<{
|
|
159
|
+
readonly key: Schema.String;
|
|
160
|
+
readonly value: Schema.String;
|
|
161
|
+
}>>;
|
|
162
|
+
}>>;
|
|
163
|
+
}>>;
|
|
164
|
+
readonly next: Schema.optional<Schema.String>;
|
|
165
|
+
}>]>, Schema.Struct<{
|
|
166
|
+
readonly options: Schema.$Array<Schema.Struct<{
|
|
167
|
+
readonly value: Schema.String;
|
|
168
|
+
readonly label: Schema.String;
|
|
169
|
+
readonly detail: Schema.optional<Schema.String>;
|
|
170
|
+
}>>;
|
|
171
|
+
readonly next: Schema.optional<Schema.String>;
|
|
172
|
+
}>]>;
|
|
173
|
+
/** A rows read or a placement-options read. */
|
|
174
|
+
export type GillReply = Schema.Schema.Type<typeof gillReplySchema>;
|
|
175
|
+
/** Decode the selected host's bounded reply before rendering or navigation. */
|
|
176
|
+
export declare const decodeGillReply: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
177
|
+
readonly state: "empty";
|
|
178
|
+
readonly title: string;
|
|
179
|
+
readonly viewId: string;
|
|
180
|
+
readonly rows: readonly [];
|
|
181
|
+
readonly next?: string | undefined;
|
|
182
|
+
} | {
|
|
183
|
+
readonly error: string;
|
|
184
|
+
readonly state: "error";
|
|
185
|
+
readonly title: string;
|
|
186
|
+
readonly viewId: string;
|
|
187
|
+
readonly rows: readonly [];
|
|
188
|
+
readonly reason?: "exec_isolation_required" | "params_required" | undefined;
|
|
189
|
+
readonly recovery?: {
|
|
190
|
+
readonly kind: "configure";
|
|
191
|
+
readonly param: string;
|
|
192
|
+
} | {
|
|
193
|
+
readonly kind: "open";
|
|
194
|
+
} | undefined;
|
|
195
|
+
} | {
|
|
196
|
+
readonly state: "full";
|
|
197
|
+
readonly title: string;
|
|
198
|
+
readonly viewId: string;
|
|
199
|
+
readonly rows: readonly {
|
|
200
|
+
readonly id: string;
|
|
201
|
+
readonly title: string;
|
|
202
|
+
readonly detail?: string | undefined;
|
|
203
|
+
readonly tone?: "error" | "running" | "attention" | "settled" | "muted" | undefined;
|
|
204
|
+
readonly open?: {
|
|
205
|
+
readonly params: readonly {
|
|
206
|
+
readonly key: string;
|
|
207
|
+
readonly value: string;
|
|
208
|
+
}[];
|
|
209
|
+
} | undefined;
|
|
210
|
+
readonly status?: {
|
|
211
|
+
readonly label: string;
|
|
212
|
+
readonly tone: "accent" | "error" | "success" | "neutral" | "primary" | "secondary" | "info" | "warning";
|
|
213
|
+
} | undefined;
|
|
214
|
+
}[];
|
|
215
|
+
readonly next?: string | undefined;
|
|
216
|
+
} | {
|
|
217
|
+
readonly options: readonly {
|
|
218
|
+
readonly value: string;
|
|
219
|
+
readonly label: string;
|
|
220
|
+
readonly detail?: string | undefined;
|
|
221
|
+
}[];
|
|
222
|
+
readonly next?: string | undefined;
|
|
223
|
+
};
|
|
224
|
+
/** Decode an options method's result without trusting extra fields. */
|
|
225
|
+
export declare const decodeGillOptionsResponse: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
226
|
+
readonly options: readonly {
|
|
227
|
+
readonly value: string;
|
|
228
|
+
readonly label: string;
|
|
229
|
+
readonly detail?: string | undefined;
|
|
230
|
+
}[];
|
|
231
|
+
readonly next?: string | undefined;
|
|
232
|
+
};
|
|
233
|
+
/** App output must match its digest-covered view identity. */
|
|
234
|
+
export declare function decodeDeclaredGillResponse(response: GillResponse, declaration: Pick<AppDeclaredGill, "id" | "params">): GillResponse;
|
|
235
|
+
/** Authenticated host Fetch stays outside App code and is pinned to one selection. */
|
|
236
|
+
export declare function createGillClient(options: Readonly<{
|
|
237
|
+
/** Decode a host's HTTP 503 body into its Gill refusal, if recognized. */
|
|
238
|
+
decodeRefusal?: (raw: Schema.Json, declaration: Readonly<{
|
|
239
|
+
id: string;
|
|
240
|
+
title: string;
|
|
241
|
+
}>) => GillResponse | undefined;
|
|
242
|
+
endpoint: string;
|
|
243
|
+
fetch: typeof globalThis.fetch;
|
|
244
|
+
declaration: Pick<AppDeclaredGill, "id" | "title" | "params">;
|
|
245
|
+
}>): {
|
|
246
|
+
/** Fetch one decoded page using this client's pinned declaration. */
|
|
247
|
+
request(selectors: GillRequest, signal?: AbortSignal): Promise<GillReply>;
|
|
248
|
+
};
|
package/dist/gill.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { appGillParamKeySchema } from "./declaration.js";
|
|
3
|
+
import { rowLabelSchemaV1 as label, rowIdentitySchemaV1 as identity, rowPageSchemaV1 as rows, } from "./rows.js";
|
|
4
|
+
/** Placement values are bounded; the admitting host checks declared keys. */
|
|
5
|
+
export const gillParamsSchema = Schema.Record(appGillParamKeySchema, Schema.String.check(Schema.isMaxLength(256))).check(Schema.makeFilter((params) => Object.keys(params).length <= 4, {
|
|
6
|
+
message: "Gill placement supports at most four parameters",
|
|
7
|
+
}));
|
|
8
|
+
/** The caller supplies placement context and a source-owned continuation. */
|
|
9
|
+
export const gillRequestSchema = Schema.Struct({
|
|
10
|
+
viewId: identity,
|
|
11
|
+
params: Schema.optional(gillParamsSchema),
|
|
12
|
+
after: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
|
|
13
|
+
options: Schema.optional(appGillParamKeySchema),
|
|
14
|
+
});
|
|
15
|
+
/** Runtime call selected by the host after admitting a declared source. */
|
|
16
|
+
export const gillCallSchema = Schema.Struct({
|
|
17
|
+
id: identity,
|
|
18
|
+
params: Schema.optional(gillParamsSchema),
|
|
19
|
+
after: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
|
|
20
|
+
options: Schema.optional(appGillParamKeySchema),
|
|
21
|
+
});
|
|
22
|
+
/** Shared response codec. Rows are plain text and cannot carry executable UI. */
|
|
23
|
+
export const gillResponseSchema = Schema.Union([
|
|
24
|
+
Schema.Struct({
|
|
25
|
+
viewId: identity,
|
|
26
|
+
title: label,
|
|
27
|
+
state: Schema.Literal("empty"),
|
|
28
|
+
rows: Schema.Tuple([]),
|
|
29
|
+
next: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMaxLength(1_024)))),
|
|
30
|
+
}),
|
|
31
|
+
Schema.Struct({
|
|
32
|
+
viewId: identity,
|
|
33
|
+
title: label,
|
|
34
|
+
state: Schema.Literal("error"),
|
|
35
|
+
rows: Schema.Tuple([]),
|
|
36
|
+
/** Host-owned permanent execution refusal; an ordinary error may still be refreshed. */
|
|
37
|
+
reason: Schema.optional(Schema.Literals(["exec_isolation_required", "params_required"])),
|
|
38
|
+
error: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(512))),
|
|
39
|
+
/** The host supplies the same-placement picker and this App's launch. */
|
|
40
|
+
recovery: Schema.optional(Schema.Union([
|
|
41
|
+
Schema.Struct({
|
|
42
|
+
kind: Schema.Literal("configure"),
|
|
43
|
+
param: appGillParamKeySchema,
|
|
44
|
+
}),
|
|
45
|
+
Schema.Struct({ kind: Schema.Literal("open") }),
|
|
46
|
+
])),
|
|
47
|
+
}),
|
|
48
|
+
Schema.Struct({
|
|
49
|
+
viewId: identity,
|
|
50
|
+
title: label,
|
|
51
|
+
state: Schema.Literal("full"),
|
|
52
|
+
rows: rows.pipe(Schema.check(Schema.isMinLength(1))),
|
|
53
|
+
next: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMaxLength(1_024)))),
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
/** Decode backend output once at the App host boundary. */
|
|
57
|
+
export const decodeGillResponse = Schema.decodeUnknownSync(gillResponseSchema, {
|
|
58
|
+
onExcessProperty: "error",
|
|
59
|
+
});
|
|
60
|
+
/** A bounded choice page returned by an App-owned options read method. */
|
|
61
|
+
export const gillOptionsResponseSchema = Schema.Struct({
|
|
62
|
+
options: Schema.Array(Schema.Struct({
|
|
63
|
+
value: Schema.String.check(Schema.isMaxLength(256)),
|
|
64
|
+
label,
|
|
65
|
+
detail: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
|
|
66
|
+
})).check(Schema.isMaxLength(200)),
|
|
67
|
+
next: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
|
|
68
|
+
});
|
|
69
|
+
/** Shared wire for a rows read or a placement-options read. */
|
|
70
|
+
export const gillReplySchema = Schema.Union([
|
|
71
|
+
gillResponseSchema,
|
|
72
|
+
gillOptionsResponseSchema,
|
|
73
|
+
]);
|
|
74
|
+
/** Decode the selected host's bounded reply before rendering or navigation. */
|
|
75
|
+
export const decodeGillReply = Schema.decodeUnknownSync(gillReplySchema, {
|
|
76
|
+
onExcessProperty: "error",
|
|
77
|
+
});
|
|
78
|
+
/** Decode an options method's result without trusting extra fields. */
|
|
79
|
+
export const decodeGillOptionsResponse = Schema.decodeUnknownSync(gillOptionsResponseSchema, { onExcessProperty: "error" });
|
|
80
|
+
/** App output must match its digest-covered view identity. */
|
|
81
|
+
export function decodeDeclaredGillResponse(response, declaration) {
|
|
82
|
+
if (response.viewId !== declaration.id)
|
|
83
|
+
throw new Error("Gill response does not match its Release declaration");
|
|
84
|
+
const recovery = response.state === "error" ? response.recovery : undefined;
|
|
85
|
+
if (recovery?.kind === "configure" &&
|
|
86
|
+
!declaration.params?.some((param) => param.key === recovery.param))
|
|
87
|
+
throw new Error("Gill recovery parameter is not declared");
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
/** Authenticated host Fetch stays outside App code and is pinned to one selection. */
|
|
91
|
+
export function createGillClient(options) {
|
|
92
|
+
return {
|
|
93
|
+
/** Fetch one decoded page using this client's pinned declaration. */
|
|
94
|
+
async request(selectors, signal) {
|
|
95
|
+
if (selectors.viewId !== options.declaration.id)
|
|
96
|
+
throw new Error("Gill request does not match its Release declaration");
|
|
97
|
+
const response = await options.fetch(options.endpoint, {
|
|
98
|
+
method: "PUT",
|
|
99
|
+
redirect: "error",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify(Schema.encodeSync(gillRequestSchema)(selectors)),
|
|
102
|
+
signal: signal ?? null,
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
if (response.status === 503) {
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = Schema.decodeUnknownSync(Schema.Json)(await response.json());
|
|
109
|
+
}
|
|
110
|
+
catch (cause) {
|
|
111
|
+
throw new Error("Gill request refused with HTTP 503", { cause });
|
|
112
|
+
}
|
|
113
|
+
const refusal = options.decodeRefusal?.(raw, options.declaration);
|
|
114
|
+
if (refusal !== undefined)
|
|
115
|
+
return refusal;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
await response.body?.cancel();
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`Gill request refused with HTTP ${String(response.status)}`);
|
|
121
|
+
}
|
|
122
|
+
const raw = await response.json();
|
|
123
|
+
const reply = decodeGillReply(raw);
|
|
124
|
+
if ("options" in reply) {
|
|
125
|
+
if (selectors.options === undefined)
|
|
126
|
+
throw new Error("Gill returned choices for a row read");
|
|
127
|
+
return reply;
|
|
128
|
+
}
|
|
129
|
+
if (selectors.options !== undefined && reply.state !== "error")
|
|
130
|
+
throw new Error("Gill returned rows for an options read");
|
|
131
|
+
return decodeDeclaredGillResponse(reply, options.declaration);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const units = [
|
|
2
|
+
["year", 31_536_000_000],
|
|
3
|
+
["month", 2_592_000_000],
|
|
4
|
+
["week", 604_800_000],
|
|
5
|
+
["day", 86_400_000],
|
|
6
|
+
["hour", 3_600_000],
|
|
7
|
+
["minute", 60_000],
|
|
8
|
+
];
|
|
9
|
+
const relative = new Intl.RelativeTimeFormat("en", {
|
|
10
|
+
numeric: "auto",
|
|
11
|
+
style: "narrow",
|
|
12
|
+
});
|
|
13
|
+
/** Render App timestamps using Bonsai's existing relative-time presentation. */
|
|
14
|
+
export function relativeTime(at, now = Date.now()) {
|
|
15
|
+
const delta = at - now;
|
|
16
|
+
for (const [unit, size] of units)
|
|
17
|
+
if (Math.abs(delta) >= size)
|
|
18
|
+
return relative.format(Math.round(delta / size), unit);
|
|
19
|
+
return "just now";
|
|
20
|
+
}
|