@arcadible/sdk 1.5.2-next.0 → 1.5.3-next.6

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.
@@ -0,0 +1,30 @@
1
+ // The game's side of `tickets`: `arcadible.tickets`, a ticket for one of
2
+ // the game's servers. The game sends it and never reads it; what it says is
3
+ // its server's to check.
4
+ import { ArcadibleError, Codes } from "../bus/protocol.js";
5
+ import { isAnswer, Names, serverOf } from "./rules.js";
6
+ // [function]
7
+ export function createTicketsClient(bus) {
8
+ const tickets = {
9
+ async get(server) {
10
+ const origin = serverOf(server);
11
+ if (origin === null) {
12
+ throw new ArcadibleError(Codes.Invalid, "tickets.get takes a server's address, such as https://api.example.com.");
13
+ }
14
+ try {
15
+ const answer = await bus.request(Names.Get, { server: origin }, isAnswer);
16
+ return answer.ticket;
17
+ }
18
+ catch (err) {
19
+ // no page to ask (standing alone), one from before tickets, or one
20
+ // that didn't answer in time: there's no ticket now.
21
+ if (err instanceof ArcadibleError &&
22
+ (err.code === Codes.Unsupported || err.code === Codes.Timeout)) {
23
+ throw new ArcadibleError(Codes.Unavailable, "A ticket can't be had right now. Try again.");
24
+ }
25
+ throw err;
26
+ }
27
+ },
28
+ };
29
+ return { tickets };
30
+ }
@@ -0,0 +1,26 @@
1
+ export declare const Names: {
2
+ readonly Get: "tickets.get";
3
+ };
4
+ export type TicketAnswer = {
5
+ ticket: string;
6
+ };
7
+ /**
8
+ * The origin of one of a game's servers, from any address of it the game
9
+ * fetches: `https://api.example.com/session` is `https://api.example.com`.
10
+ * A ticket is for the origin, so the path never matters. Any web address
11
+ * has one; which servers get a ticket is the page's to say (a play page
12
+ * takes `https` ones the game declared, a local one the developer's own
13
+ * machine too), so a game's copy of this never needs to change. `null` for
14
+ * what isn't a web address.
15
+ */
16
+ export declare function serverOf(value: unknown): string | null;
17
+ /** Whether a value is a ticket, as either end hands one over. */
18
+ export declare function isTicket(value: unknown): value is string;
19
+ /** Whether a value is the page's answer: a ticket. */
20
+ export declare function isAnswer(value: unknown): value is TicketAnswer;
21
+ /**
22
+ * How long a ticket is good for, in milliseconds, from its own `iat` and
23
+ * `exp`, read without checking it: the page only decides how long to keep
24
+ * it, and never trusts it. `null` when it doesn't say.
25
+ */
26
+ export declare function lifetimeOf(ticket: string): number | null;
@@ -0,0 +1,76 @@
1
+ // `tickets` on the bus, shared by both ends: a ticket for one of the game's
2
+ // servers, asked for by its address.
3
+ // [import]
4
+ import { isObject } from "../bus/protocol.js";
5
+ // [declare]
6
+ export const Names = {
7
+ // a ticket, a request with `{ server }`, an origin, answered `{ ticket }`.
8
+ Get: "tickets.get",
9
+ };
10
+ // The longest address a game asks with, and the largest ticket a page
11
+ // hands over: a ticket is a few hundred bytes.
12
+ const MAX_SERVER = 2048;
13
+ const MAX_TICKET = 4096;
14
+ // A ticket is a JWT: three base64url parts.
15
+ const TICKET = /^[\w-]+\.[\w-]+\.[\w-]+$/u;
16
+ // [function]
17
+ /**
18
+ * The origin of one of a game's servers, from any address of it the game
19
+ * fetches: `https://api.example.com/session` is `https://api.example.com`.
20
+ * A ticket is for the origin, so the path never matters. Any web address
21
+ * has one; which servers get a ticket is the page's to say (a play page
22
+ * takes `https` ones the game declared, a local one the developer's own
23
+ * machine too), so a game's copy of this never needs to change. `null` for
24
+ * what isn't a web address.
25
+ */
26
+ export function serverOf(value) {
27
+ if (typeof value !== "string" || value.length > MAX_SERVER)
28
+ return null;
29
+ let url;
30
+ try {
31
+ url = new URL(value);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ if (url.username !== "" || url.password !== "")
37
+ return null;
38
+ if (url.protocol !== "https:" && url.protocol !== "http:")
39
+ return null;
40
+ return url.origin;
41
+ }
42
+ // [function]
43
+ /** Whether a value is a ticket, as either end hands one over. */
44
+ export function isTicket(value) {
45
+ return (typeof value === "string" &&
46
+ value.length <= MAX_TICKET &&
47
+ TICKET.test(value));
48
+ }
49
+ // [function]
50
+ /** Whether a value is the page's answer: a ticket. */
51
+ export function isAnswer(value) {
52
+ return isObject(value) && isTicket(value["ticket"]);
53
+ }
54
+ // [function]
55
+ /**
56
+ * How long a ticket is good for, in milliseconds, from its own `iat` and
57
+ * `exp`, read without checking it: the page only decides how long to keep
58
+ * it, and never trusts it. `null` when it doesn't say.
59
+ */
60
+ export function lifetimeOf(ticket) {
61
+ try {
62
+ const part = (ticket.split(".")[1] ?? "")
63
+ .replace(/-/gu, "+")
64
+ .replace(/_/gu, "/");
65
+ const claims = JSON.parse(atob(part));
66
+ if (!isObject(claims))
67
+ return null;
68
+ const { exp, iat } = claims;
69
+ if (typeof exp !== "number" || typeof iat !== "number")
70
+ return null;
71
+ return exp > iat ? (exp - iat) * 1000 : null;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
@@ -0,0 +1,7 @@
1
+ import type { Module } from "../bus/server.js";
2
+ export type TicketsHost = {
3
+ mint(server: string): Promise<string>;
4
+ now?: () => number;
5
+ };
6
+ export declare const TICKET_MARGIN = 60000;
7
+ export declare function ticketsServer(host: TicketsHost): () => Module;
@@ -0,0 +1,76 @@
1
+ // The page's side of `tickets`: a ticket for one of the game's servers,
2
+ // from wherever the page gets them (`mint`), kept while it's good. The page
3
+ // holds no key and reads no ticket: it asks, and hands over what came back.
4
+ // [import]
5
+ import { ArcadibleError, Codes, isObject, log, } from "../bus/protocol.js";
6
+ import { isTicket, lifetimeOf, Names, serverOf } from "./rules.js";
7
+ // A ticket is let go this long before it expires, so one handed over is
8
+ // still good when it reaches the game's server.
9
+ export const TICKET_MARGIN = 60_000;
10
+ // The codes a page's `mint` may refuse with, which reach the game as they
11
+ // are; anything else is `unavailable`.
12
+ const REFUSALS = new Set([
13
+ Codes.Busy,
14
+ Codes.Invalid,
15
+ Codes.Unavailable,
16
+ ]);
17
+ const REFUSED = "The play page couldn't give a ticket.";
18
+ // [function]
19
+ export function ticketsServer(host) {
20
+ const now = host.now ?? (() => Date.now());
21
+ // by server, for the page's life: who's playing never changes under a
22
+ // page, so a game that reloads in its frame is handed the same ticket.
23
+ const kept = new Map();
24
+ const asked = new Map();
25
+ const mint = async function (server) {
26
+ let ticket;
27
+ const at = now();
28
+ try {
29
+ ticket = await host.mint(server);
30
+ }
31
+ catch (err) {
32
+ if (err instanceof ArcadibleError)
33
+ throw err;
34
+ const refused = isObject(err) ? err["code"] : undefined;
35
+ if (typeof refused === "string" && REFUSALS.has(refused)) {
36
+ throw new ArcadibleError(refused, REFUSED);
37
+ }
38
+ log("warn", "a ticket couldn't be made.", err);
39
+ throw new ArcadibleError(Codes.Unavailable, "A ticket can't be had right now. Try again.");
40
+ }
41
+ if (!isTicket(ticket)) {
42
+ log("error", "the page's ticket isn't one.");
43
+ throw new ArcadibleError(Codes.Unavailable, "A ticket can't be had right now. Try again.");
44
+ }
45
+ // kept by its own lifetime, counted from when it was asked for, so the
46
+ // device's clock never matters; one that doesn't say isn't kept.
47
+ const lifetime = lifetimeOf(ticket);
48
+ if (lifetime !== null && lifetime > TICKET_MARGIN) {
49
+ kept.set(server, { ticket, until: at + lifetime - TICKET_MARGIN });
50
+ }
51
+ return ticket;
52
+ };
53
+ const get = async function (params) {
54
+ const server = serverOf(params["server"]);
55
+ if (server === null || server !== params["server"]) {
56
+ throw new ArcadibleError(Codes.Invalid, "tickets.get takes a server's origin.");
57
+ }
58
+ const held = kept.get(server);
59
+ if (held && now() < held.until)
60
+ return { ticket: held.ticket };
61
+ kept.delete(server);
62
+ // calls made at once are one mint.
63
+ let asking = asked.get(server);
64
+ if (!asking) {
65
+ asking = mint(server).finally(() => asked.delete(server));
66
+ asked.set(server, asking);
67
+ }
68
+ return { ticket: await asking };
69
+ };
70
+ return () => ({
71
+ handlers: {
72
+ // beside the queue: it waits on the network, not on storage.
73
+ [Names.Get]: { run: get, parallel: true },
74
+ },
75
+ });
76
+ }
@@ -0,0 +1,67 @@
1
+ import type { Bus } from "../bus/client.js";
2
+ import type { Player } from "../players/rules.js";
3
+ import { type Entry, type Item, type Listed, type ListOptions, type Value, type Written } from "./rules.js";
4
+ /**
5
+ * Whose the values are: `"developer"`, what the game's developer published,
6
+ * and `"game"`, what the game shares with every player, each to read; or a
7
+ * player, `arcadible.players.primary`, whose own the game reads and writes.
8
+ */
9
+ export type Scope = "developer" | "game" | Player;
10
+ /** A scope's values to read: `values`' calls, with the scope already named. */
11
+ export interface ReadableValues {
12
+ read(key: string): Promise<Value | null>;
13
+ read(keys: readonly string[]): Promise<(Value | null)[]>;
14
+ list(options?: ListOptions): Promise<Listed>;
15
+ }
16
+ /** A player's values to read and write: `values`' calls, with the player already named. */
17
+ export interface ScopedValues extends ReadableValues {
18
+ write(key: string, value: unknown, options?: {
19
+ revision?: number;
20
+ }): Promise<Written>;
21
+ write(items: readonly Item[]): Promise<Written[]>;
22
+ remove(key: string | readonly string[]): Promise<void>;
23
+ }
24
+ /** The game's data kept on Arcadible. */
25
+ export interface Values {
26
+ /**
27
+ * The value under a key, with its revision, or `null` for a key with
28
+ * none. Rejects with `denied` for a player's values while a guest plays,
29
+ * or for a player who isn't `players.primary`; `busy` (too many calls);
30
+ * or `unavailable` (offline, or off Arcadible; worth trying again).
31
+ */
32
+ read(scope: Scope, key: string): Promise<Value | null>;
33
+ /** The values under the keys, in their order, in one trip: each `null` for a key with none. */
34
+ read(scope: Scope, keys: readonly string[]): Promise<(Value | null)[]>;
35
+ /**
36
+ * Replaces a key's value, whole, in the player's own values. With
37
+ * `revision`, only if it's the value's now (`0`: only if there's none);
38
+ * otherwise it rejects with `conflict`. Also rejects with `too_large` (a
39
+ * value past 64 KB) and `full` (the player's storage is used up).
40
+ */
41
+ write(scope: Player, key: string, value: unknown, options?: {
42
+ revision?: number;
43
+ }): Promise<Written>;
44
+ /** Writes several of the player's values in one trip, all of them or none. */
45
+ write(scope: Player, items: readonly Item[]): Promise<Written[]>;
46
+ /** Removes what the key, or each of the keys, holds in the player's values, if anything. */
47
+ remove(scope: Player, key: string | readonly string[]): Promise<void>;
48
+ /**
49
+ * A scope's keys in order, by byte, each with its revision, time, and
50
+ * size, never its value: those that start with `prefix`, after the key
51
+ * `after`, 50 by default and 100 at most. `next` is the key to pass as
52
+ * `after` for the following page, or `null` at the end.
53
+ */
54
+ list(scope: Scope, options?: ListOptions): Promise<Listed>;
55
+ /**
56
+ * The same calls for one scope, named once:
57
+ * `values.scope(player).read(key)` is `values.read(player, key)`. The
58
+ * developer's and the game's have only `read` and `list`.
59
+ */
60
+ scope(scope: "developer" | "game"): ReadableValues;
61
+ scope(scope: Player): ScopedValues;
62
+ }
63
+ export type ValuesClient = {
64
+ values: Values;
65
+ };
66
+ export type { Entry, Item, Listed, ListOptions, Value, Written };
67
+ export declare function createValuesClient(bus: Bus): ValuesClient;
@@ -0,0 +1,184 @@
1
+ // The game's side of `values`: `arcadible.values`, the game's data kept on
2
+ // Arcadible, JSON under a key, in a scope. A game reads what its developer
3
+ // published and what the game shares, and reads and writes its own
4
+ // player's, wherever they play.
5
+ import { ArcadibleError, Codes, isObject } from "../bus/protocol.js";
6
+ import { KEY, LIST_DEFAULT, listedOf, MAX_KEYS, MAX_LIST, Names, PREFIX, valuesOf, writtenOf, } from "./rules.js";
7
+ const UNAVAILABLE = "The game's values can't be reached right now. Try again.";
8
+ // [function]
9
+ function invalid(message) {
10
+ return new ArcadibleError(Codes.Invalid, message);
11
+ }
12
+ // [function]
13
+ // what a call says of its scope: its name, and the player's id for a
14
+ // player's.
15
+ function scopeOf(scope) {
16
+ if (scope === "developer" || scope === "game")
17
+ return { scope };
18
+ if (isObject(scope) && typeof scope["id"] === "string") {
19
+ return { player: scope["id"], scope: "player" };
20
+ }
21
+ throw invalid('A scope is "developer", "game", or a player: arcadible.players.primary.');
22
+ }
23
+ // [function]
24
+ // a call's keys: at least one, no more than a call takes, each a key, none
25
+ // twice.
26
+ function keysOf(keys) {
27
+ if (keys.length === 0)
28
+ throw invalid("A call names at least one key.");
29
+ if (keys.length > MAX_KEYS) {
30
+ throw new ArcadibleError(Codes.TooMany, `A call names up to ${MAX_KEYS} keys.`);
31
+ }
32
+ for (const key of keys) {
33
+ if (typeof key !== "string" || !KEY.test(key)) {
34
+ throw invalid("A key is 1 to 128 letters, digits, -, _, ., and /: " +
35
+ JSON.stringify(key));
36
+ }
37
+ }
38
+ if (new Set(keys).size !== keys.length) {
39
+ throw invalid("A call names each key once.");
40
+ }
41
+ return keys;
42
+ }
43
+ // [function]
44
+ // a value as JSON keeps it: `undefined`, a function, or a number that isn't
45
+ // one would be dropped or changed on the way, so each is refused here.
46
+ function jsonOf(key, value) {
47
+ let text;
48
+ try {
49
+ text = JSON.stringify(value, (_, each) => {
50
+ if (typeof each === "number" && !Number.isFinite(each)) {
51
+ throw new TypeError("not finite");
52
+ }
53
+ return each;
54
+ });
55
+ }
56
+ catch {
57
+ text = undefined;
58
+ }
59
+ if (text === undefined) {
60
+ throw invalid(`The value for ${JSON.stringify(key)} isn't JSON.`);
61
+ }
62
+ return JSON.parse(text);
63
+ }
64
+ // [function]
65
+ // the page's answer: `{ text }`, its server's answer as JSON text.
66
+ function isAnswer(value) {
67
+ return isObject(value) && typeof value["text"] === "string";
68
+ }
69
+ // [function]
70
+ export function createValuesClient(bus) {
71
+ // a call on the bus, answered with the page's server's data. With no page
72
+ // to ask (standing alone), one from before values, or one that didn't
73
+ // answer in time, the values can't be reached now.
74
+ const ask = async function (name, params) {
75
+ let text;
76
+ try {
77
+ // as text, so the call's size on the bus is its JSON's
78
+ text = (await bus.request(name, { body: JSON.stringify(params) }, isAnswer)).text;
79
+ }
80
+ catch (err) {
81
+ if (err instanceof ArcadibleError &&
82
+ (err.code === Codes.Unsupported || err.code === Codes.Timeout)) {
83
+ throw new ArcadibleError(Codes.Unavailable, UNAVAILABLE);
84
+ }
85
+ throw err;
86
+ }
87
+ // the page's server's answer, `{ data }`
88
+ let answer;
89
+ try {
90
+ answer = JSON.parse(text);
91
+ }
92
+ catch {
93
+ answer = null;
94
+ }
95
+ if (!isObject(answer) || !("data" in answer)) {
96
+ throw new ArcadibleError(Codes.Unavailable, UNAVAILABLE);
97
+ }
98
+ return answer["data"];
99
+ };
100
+ const unreadable = () => new ArcadibleError(Codes.Unavailable, UNAVAILABLE);
101
+ const read = async function (scope, key) {
102
+ const many = Array.isArray(key);
103
+ const keys = keysOf(many ? key : [key]);
104
+ const values = valuesOf(await ask(Names.Read, { ...scopeOf(scope), keys }), keys.length);
105
+ if (!values)
106
+ throw unreadable();
107
+ return many ? values : (values[0] ?? null);
108
+ };
109
+ const write = async function (scope, first, value, options) {
110
+ const many = Array.isArray(first);
111
+ const given = many
112
+ ? first
113
+ : [
114
+ {
115
+ key: first,
116
+ value,
117
+ ...(options?.revision === undefined
118
+ ? {}
119
+ : { revision: options.revision }),
120
+ },
121
+ ];
122
+ if (!given.every(isObject)) {
123
+ throw invalid("An item to write is { key, value }, with a revision or not.");
124
+ }
125
+ const keys = keysOf(given.map((item) => item.key));
126
+ const items = given.map((item, at) => {
127
+ const { revision } = item;
128
+ if (revision !== undefined &&
129
+ (!Number.isSafeInteger(revision) || revision < 0)) {
130
+ throw invalid("A revision is a value's own, from a read or a write, or 0.");
131
+ }
132
+ return {
133
+ key: keys[at] ?? "",
134
+ value: jsonOf(keys[at] ?? "", item.value),
135
+ ...(revision === undefined ? {} : { revision }),
136
+ };
137
+ });
138
+ const written = writtenOf(await ask(Names.Write, { ...scopeOf(scope), items }), items.length);
139
+ const [only] = written ?? [];
140
+ if (!written || !only)
141
+ throw unreadable();
142
+ return many ? written : only;
143
+ };
144
+ const remove = async function (scope, key) {
145
+ const keys = keysOf(Array.isArray(key) ? key : [key]);
146
+ await ask(Names.Remove, { ...scopeOf(scope), keys });
147
+ };
148
+ const list = async function (scope, options = {}) {
149
+ const { after = "", limit = LIST_DEFAULT, prefix = "" } = options;
150
+ if (typeof prefix !== "string" || !PREFIX.test(prefix)) {
151
+ throw invalid("A prefix is what a key starts with: a key's letters.");
152
+ }
153
+ if (typeof after !== "string" || !PREFIX.test(after)) {
154
+ throw invalid("`after` is a key: a list's `next`.");
155
+ }
156
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST) {
157
+ throw invalid(`A list gives 1 to ${MAX_LIST} keys.`);
158
+ }
159
+ const listed = listedOf(await ask(Names.List, { ...scopeOf(scope), after, limit, prefix }));
160
+ if (!listed)
161
+ throw unreadable();
162
+ return listed;
163
+ };
164
+ // the same calls with the scope named once: nothing of its own. Every
165
+ // scope's has all four at run time, and the types leave `write` and
166
+ // `remove` off the developer's and the game's, which Arcadible refuses a
167
+ // game anyway.
168
+ const scoped = function (scope) {
169
+ return {
170
+ list: (options) => list(scope, options),
171
+ read: ((key) => read(scope, key)),
172
+ remove: (key) => remove(scope, key),
173
+ write: ((first, value, options) => write(scope, first, value, options)),
174
+ };
175
+ };
176
+ const values = {
177
+ list,
178
+ read: read,
179
+ remove,
180
+ scope: scoped,
181
+ write: write,
182
+ };
183
+ return { values };
184
+ }
@@ -0,0 +1,55 @@
1
+ export declare const Names: {
2
+ readonly List: "values.list";
3
+ readonly Read: "values.read";
4
+ readonly Remove: "values.remove";
5
+ readonly Write: "values.write";
6
+ };
7
+ /** A value, with the revision a write may name to replace it. */
8
+ export type Value = {
9
+ key: string;
10
+ value: unknown;
11
+ revision: number;
12
+ /** When it was written, as an ISO time. */
13
+ updatedAt: string;
14
+ };
15
+ /** What a write left: the value's new revision. */
16
+ export type Written = {
17
+ key: string;
18
+ revision: number;
19
+ updatedAt: string;
20
+ };
21
+ /** A key in a list, without its value: its size is the value's, in bytes. */
22
+ export type Entry = {
23
+ key: string;
24
+ revision: number;
25
+ updatedAt: string;
26
+ size: number;
27
+ };
28
+ /** A value to write. With `revision`, only if it's the value's now; `0`, only if there's none. */
29
+ export type Item = {
30
+ key: string;
31
+ value: unknown;
32
+ revision?: number;
33
+ };
34
+ /** What a list takes: a prefix, the key to go on after, and how many. */
35
+ export type ListOptions = {
36
+ prefix?: string;
37
+ after?: string;
38
+ limit?: number;
39
+ };
40
+ /** A list's page: `next` is the key to pass as `after`, or `null` at the end. */
41
+ export type Listed = {
42
+ entries: Entry[];
43
+ next: string | null;
44
+ };
45
+ export declare const KEY: RegExp;
46
+ export declare const PREFIX: RegExp;
47
+ export declare const MAX_KEYS = 32;
48
+ export declare const MAX_LIST = 100;
49
+ export declare const LIST_DEFAULT = 50;
50
+ /** A read's answer for `count` keys: each a value or `null`, or `null` for what isn't one. */
51
+ export declare function valuesOf(data: unknown, count: number): (Value | null)[] | null;
52
+ /** A write's answer for `count` items, or `null` for what isn't one. */
53
+ export declare function writtenOf(data: unknown, count: number): Written[] | null;
54
+ /** A list's answer, or `null` for what isn't one. */
55
+ export declare function listedOf(data: unknown): Listed | null;
@@ -0,0 +1,104 @@
1
+ // `values` on the bus, shared by both ends: a game's data kept on
2
+ // Arcadible, JSON under a key, in a scope. A game reads the developer's
3
+ // and the game's, and reads and writes its own player's.
4
+ // [import]
5
+ import { isObject } from "../bus/protocol.js";
6
+ // [declare]
7
+ export const Names = {
8
+ // each a request with `{ body }`, the call as JSON text: its scope
9
+ // (`scope`, and `player`, an id, for a player's), and its keys, items, or
10
+ // a list's options; answered `{ text }`, the page's server's answer as it
11
+ // came. As text, a call's size on the bus is its JSON's, which is what
12
+ // the limits count, and the page reads neither.
13
+ List: "values.list",
14
+ Read: "values.read",
15
+ Remove: "values.remove",
16
+ Write: "values.write",
17
+ };
18
+ // A key: 1 to 128 letters, digits, `-`, `_`, `.`, and `/`; and what one may
19
+ // start with.
20
+ export const KEY = /^[A-Za-z0-9_./-]{1,128}$/u;
21
+ export const PREFIX = /^[A-Za-z0-9_./-]{0,128}$/u;
22
+ // The most keys one call names, and one list gives.
23
+ export const MAX_KEYS = 32;
24
+ export const MAX_LIST = 100;
25
+ export const LIST_DEFAULT = 50;
26
+ // [function]
27
+ // a row of an answer, as the page's server sends it, in the game's words:
28
+ // `null` for one that isn't a row.
29
+ function rowOf(entry) {
30
+ if (!isObject(entry) ||
31
+ typeof entry["key"] !== "string" ||
32
+ typeof entry["revision"] !== "number" ||
33
+ typeof entry["updated_at"] !== "string") {
34
+ return null;
35
+ }
36
+ return {
37
+ key: entry["key"],
38
+ revision: entry["revision"],
39
+ size: typeof entry["size"] === "number" ? entry["size"] : 0,
40
+ updatedAt: entry["updated_at"],
41
+ value: entry["value"],
42
+ };
43
+ }
44
+ // [function]
45
+ /** A read's answer for `count` keys: each a value or `null`, or `null` for what isn't one. */
46
+ export function valuesOf(data, count) {
47
+ if (!Array.isArray(data) || data.length !== count)
48
+ return null;
49
+ const values = [];
50
+ for (const entry of data) {
51
+ if (entry === null) {
52
+ values.push(null);
53
+ continue;
54
+ }
55
+ const row = rowOf(entry);
56
+ if (!row)
57
+ return null;
58
+ values.push({
59
+ key: row.key,
60
+ revision: row.revision,
61
+ updatedAt: row.updatedAt,
62
+ value: row.value,
63
+ });
64
+ }
65
+ return values;
66
+ }
67
+ // [function]
68
+ /** A write's answer for `count` items, or `null` for what isn't one. */
69
+ export function writtenOf(data, count) {
70
+ if (!Array.isArray(data) || data.length !== count)
71
+ return null;
72
+ const written = [];
73
+ for (const entry of data) {
74
+ const row = rowOf(entry);
75
+ if (!row)
76
+ return null;
77
+ written.push({
78
+ key: row.key,
79
+ revision: row.revision,
80
+ updatedAt: row.updatedAt,
81
+ });
82
+ }
83
+ return written;
84
+ }
85
+ // [function]
86
+ /** A list's answer, or `null` for what isn't one. */
87
+ export function listedOf(data) {
88
+ if (!isObject(data) || !Array.isArray(data["entries"]))
89
+ return null;
90
+ const entries = [];
91
+ for (const entry of data["entries"]) {
92
+ const row = rowOf(entry);
93
+ if (!row)
94
+ return null;
95
+ entries.push({
96
+ key: row.key,
97
+ revision: row.revision,
98
+ size: row.size,
99
+ updatedAt: row.updatedAt,
100
+ });
101
+ }
102
+ const next = data["next"];
103
+ return { entries, next: typeof next === "string" ? next : null };
104
+ }
@@ -0,0 +1,7 @@
1
+ import type { Module } from "../bus/server.js";
2
+ /** A call's verb, as the page's server names its routes. */
3
+ export type Verb = "list" | "read" | "remove" | "write";
4
+ export type ValuesHost = {
5
+ call(verb: Verb, body: string): Promise<string>;
6
+ };
7
+ export declare function valuesServer(host: ValuesHost): () => Module;