@frockbot/applet-sdk 0.0.0 → 0.3.13

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,398 @@
1
+ /**
2
+ * The SQLite side of an Applet: DDL, typed rows, and the change log.
3
+ *
4
+ * Everything here is synchronous against a `SqlStorage`-shaped handle, so the
5
+ * caller can wrap a whole client transaction in `ctx.storage.transactionSync`
6
+ * and so the whole module is testable against any SQLite driver.
7
+ */
8
+
9
+ import {
10
+ AppletValidationError,
11
+ addColumnStatement,
12
+ createTableStatement,
13
+ decodeValue,
14
+ encodeValue,
15
+ quoteIdentifier,
16
+ schemaFingerprint,
17
+ type SqlValue,
18
+ type TableDefinition,
19
+ type TablesShape,
20
+ } from "../schema/index.js";
21
+ import type { AppletChangeV1, AppletMutationV1 } from "../protocol/index.js";
22
+
23
+ /** The subset of Cloudflare's `SqlStorage` the SDK uses. */
24
+ export interface AppletSqlStorage {
25
+ exec(
26
+ query: string,
27
+ ...bindings: unknown[]
28
+ ): { toArray(): Array<Record<string, unknown>> };
29
+ }
30
+
31
+ const META_TABLE = "_applet_meta";
32
+ const CHANGE_TABLE = "_applet_changes";
33
+ /** Retained change-log length; a client further behind gets a full snapshot. */
34
+ export const CHANGE_LOG_LIMIT = 2_000;
35
+
36
+ export interface SchemaState {
37
+ revision: number;
38
+ /** True when this mount changed the declared shape. */
39
+ changed: boolean;
40
+ previousRevision: number;
41
+ }
42
+
43
+ export class AppletStore {
44
+ private readonly sql: AppletSqlStorage;
45
+ readonly tables: TablesShape;
46
+ private cursor = 0;
47
+
48
+ constructor(sql: AppletSqlStorage, tables: TablesShape) {
49
+ this.sql = sql;
50
+ this.tables = tables;
51
+ }
52
+
53
+ /**
54
+ * Idempotent DDL. Creates the SDK's own tables and every declared table,
55
+ * adds columns declared since the last mount, and reports whether the shape
56
+ * moved so the caller can run `migrate`.
57
+ */
58
+ ensureSchema(): SchemaState {
59
+ this.sql.exec(
60
+ `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(META_TABLE)} ("key" TEXT PRIMARY KEY NOT NULL, "value" TEXT NOT NULL)`,
61
+ );
62
+ this.sql.exec(
63
+ `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(CHANGE_TABLE)} (` +
64
+ `"seq" INTEGER PRIMARY KEY AUTOINCREMENT, "txnId" TEXT, "tbl" TEXT NOT NULL, ` +
65
+ `"op" TEXT NOT NULL, "rowKey" TEXT NOT NULL, "row" TEXT, "at" TEXT NOT NULL)`,
66
+ );
67
+
68
+ for (const [name, definition] of Object.entries(this.tables)) {
69
+ this.sql.exec(createTableStatement(name, definition));
70
+ const existing = new Set(
71
+ this.sql
72
+ .exec(`PRAGMA table_info(${quoteIdentifier(name)})`)
73
+ .toArray()
74
+ .map((column) => String(column.name)),
75
+ );
76
+ for (const [column, spec] of Object.entries(definition.columns)) {
77
+ if (existing.has(column)) continue;
78
+ this.sql.exec(addColumnStatement(name, column, spec));
79
+ }
80
+ }
81
+
82
+ this.cursor = Number(
83
+ this.sql
84
+ .exec(`SELECT MAX("seq") AS seq FROM ${quoteIdentifier(CHANGE_TABLE)}`)
85
+ .toArray()[0]?.seq ?? 0,
86
+ );
87
+
88
+ const fingerprint = schemaFingerprint(this.tables);
89
+ const storedFingerprint = this.readMeta("schema:fingerprint");
90
+ const previousRevision = Number(this.readMeta("schema:revision") ?? 0);
91
+ if (storedFingerprint === fingerprint) {
92
+ return { revision: previousRevision, changed: false, previousRevision };
93
+ }
94
+ const revision = previousRevision + 1;
95
+ this.writeMeta("schema:fingerprint", fingerprint);
96
+ this.writeMeta("schema:revision", String(revision));
97
+ return { revision, changed: true, previousRevision };
98
+ }
99
+
100
+ private readMeta(key: string): string | undefined {
101
+ const rows = this.sql
102
+ .exec(
103
+ `SELECT "value" FROM ${quoteIdentifier(META_TABLE)} WHERE "key" = ?`,
104
+ key,
105
+ )
106
+ .toArray();
107
+ return rows.length === 0 ? undefined : String(rows[0]!.value);
108
+ }
109
+
110
+ private writeMeta(key: string, value: string): void {
111
+ this.sql.exec(
112
+ `INSERT INTO ${quoteIdentifier(META_TABLE)} ("key", "value") VALUES (?, ?) ` +
113
+ `ON CONFLICT("key") DO UPDATE SET "value" = excluded."value"`,
114
+ key,
115
+ value,
116
+ );
117
+ }
118
+
119
+ get lastChangeId(): number {
120
+ return this.cursor;
121
+ }
122
+
123
+ /** Fail closed on a table the Applet did not declare. */
124
+ private definition(table: string): TableDefinition {
125
+ const definition = this.tables[table];
126
+ if (!definition) {
127
+ throw new AppletValidationError(`Unknown table "${table}"`);
128
+ }
129
+ return definition;
130
+ }
131
+
132
+ private decodeRow(
133
+ definition: TableDefinition,
134
+ raw: Record<string, unknown>,
135
+ ): Record<string, unknown> {
136
+ const row: Record<string, unknown> = {};
137
+ for (const [column, spec] of Object.entries(definition.columns)) {
138
+ row[column] = decodeValue(spec.definition, raw[column]);
139
+ }
140
+ return row;
141
+ }
142
+
143
+ private encodeInsert(
144
+ table: string,
145
+ definition: TableDefinition,
146
+ values: Record<string, unknown>,
147
+ suppliedKey?: string,
148
+ ): { key: string; columns: string[]; bindings: SqlValue[] } {
149
+ for (const column of Object.keys(values)) {
150
+ if (!Object.hasOwn(definition.columns, column)) {
151
+ throw new AppletValidationError(`Unknown column "${table}.${column}"`);
152
+ }
153
+ }
154
+ const columns: string[] = [];
155
+ const bindings: SqlValue[] = [];
156
+ let key = suppliedKey;
157
+ for (const [column, spec] of Object.entries(definition.columns)) {
158
+ const meta = spec.definition;
159
+ let value = values[column];
160
+ if (column === definition.primaryKey) {
161
+ value =
162
+ (value as string | undefined) ?? suppliedKey ?? crypto.randomUUID();
163
+ key = value as string;
164
+ } else if (value === undefined) {
165
+ if (meta.hasDefault) value = meta.defaultValue;
166
+ else if (meta.optional) value = null;
167
+ else {
168
+ throw new AppletValidationError(
169
+ `Column "${table}.${column}" is required`,
170
+ );
171
+ }
172
+ }
173
+ columns.push(column);
174
+ bindings.push(encodeValue(`${table}.${column}`, meta, value));
175
+ }
176
+ return { key: key!, columns, bindings };
177
+ }
178
+
179
+ insert(
180
+ table: string,
181
+ values: Record<string, unknown>,
182
+ txnId?: string,
183
+ ): AppletChangeV1 {
184
+ const definition = this.definition(table);
185
+ const { key, columns, bindings } = this.encodeInsert(
186
+ table,
187
+ definition,
188
+ values,
189
+ );
190
+ this.sql.exec(
191
+ `INSERT INTO ${quoteIdentifier(table)} (${columns.map(quoteIdentifier).join(", ")}) ` +
192
+ `VALUES (${columns.map(() => "?").join(", ")})`,
193
+ ...bindings,
194
+ );
195
+ const row = this.read(table, key);
196
+ if (!row)
197
+ throw new AppletValidationError(`Insert into "${table}" did not persist`);
198
+ return this.log({ table, op: "insert", key, row }, txnId);
199
+ }
200
+
201
+ update(
202
+ table: string,
203
+ key: string,
204
+ patch: Record<string, unknown>,
205
+ txnId?: string,
206
+ ): AppletChangeV1 | undefined {
207
+ const definition = this.definition(table);
208
+ const assignments: string[] = [];
209
+ const bindings: SqlValue[] = [];
210
+ for (const [column, value] of Object.entries(patch)) {
211
+ if (!Object.hasOwn(definition.columns, column)) {
212
+ throw new AppletValidationError(`Unknown column "${table}.${column}"`);
213
+ }
214
+ if (column === definition.primaryKey) {
215
+ throw new AppletValidationError(
216
+ `Column "${table}.${column}" is the key`,
217
+ );
218
+ }
219
+ assignments.push(`${quoteIdentifier(column)} = ?`);
220
+ bindings.push(
221
+ encodeValue(
222
+ `${table}.${column}`,
223
+ definition.columns[column]!.definition,
224
+ value,
225
+ ),
226
+ );
227
+ }
228
+ if (assignments.length === 0) return undefined;
229
+ if (!this.read(table, key)) return undefined;
230
+ this.sql.exec(
231
+ `UPDATE ${quoteIdentifier(table)} SET ${assignments.join(", ")} ` +
232
+ `WHERE ${quoteIdentifier(definition.primaryKey)} = ?`,
233
+ ...bindings,
234
+ key,
235
+ );
236
+ const row = this.read(table, key);
237
+ if (!row) return undefined;
238
+ return this.log({ table, op: "update", key, row }, txnId);
239
+ }
240
+
241
+ delete(
242
+ table: string,
243
+ key: string,
244
+ txnId?: string,
245
+ ): AppletChangeV1 | undefined {
246
+ const definition = this.definition(table);
247
+ if (!this.read(table, key)) return undefined;
248
+ this.sql.exec(
249
+ `DELETE FROM ${quoteIdentifier(table)} WHERE ${quoteIdentifier(definition.primaryKey)} = ?`,
250
+ key,
251
+ );
252
+ return this.log({ table, op: "delete", key }, txnId);
253
+ }
254
+
255
+ read(table: string, key: string): Record<string, unknown> | undefined {
256
+ const definition = this.definition(table);
257
+ const rows = this.sql
258
+ .exec(
259
+ `SELECT * FROM ${quoteIdentifier(table)} WHERE ${quoteIdentifier(definition.primaryKey)} = ? LIMIT 1`,
260
+ key,
261
+ )
262
+ .toArray();
263
+ return rows.length === 0 ? undefined : this.decodeRow(definition, rows[0]!);
264
+ }
265
+
266
+ /** Every row, or the rows whose declared columns all equal `filter`. */
267
+ select(
268
+ table: string,
269
+ filter?: Record<string, unknown>,
270
+ ): Array<Record<string, unknown>> {
271
+ const definition = this.definition(table);
272
+ const clauses: string[] = [];
273
+ const bindings: SqlValue[] = [];
274
+ for (const [column, value] of Object.entries(filter ?? {})) {
275
+ if (!Object.hasOwn(definition.columns, column)) {
276
+ throw new AppletValidationError(`Unknown column "${table}.${column}"`);
277
+ }
278
+ const encoded = encodeValue(
279
+ `${table}.${column}`,
280
+ definition.columns[column]!.definition,
281
+ value,
282
+ );
283
+ if (encoded === null) {
284
+ clauses.push(`${quoteIdentifier(column)} IS NULL`);
285
+ } else {
286
+ clauses.push(`${quoteIdentifier(column)} = ?`);
287
+ bindings.push(encoded);
288
+ }
289
+ }
290
+ const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
291
+ return this.sql
292
+ .exec(`SELECT * FROM ${quoteIdentifier(table)}${where}`, ...bindings)
293
+ .toArray()
294
+ .map((row) => this.decodeRow(definition, row));
295
+ }
296
+
297
+ /** Apply one client transaction. The caller wraps this in a SQL transaction. */
298
+ applyMutations(
299
+ mutations: AppletMutationV1[],
300
+ txnId?: string,
301
+ ): AppletChangeV1[] {
302
+ return mutations.map((mutation) => {
303
+ const change =
304
+ mutation.op === "insert"
305
+ ? this.insert(
306
+ mutation.table,
307
+ mutation.key === undefined
308
+ ? (mutation.value ?? {})
309
+ : {
310
+ ...(mutation.value ?? {}),
311
+ [this.definition(mutation.table).primaryKey]: mutation.key,
312
+ },
313
+ txnId,
314
+ )
315
+ : mutation.op === "update"
316
+ ? this.update(
317
+ mutation.table,
318
+ mutation.key!,
319
+ mutation.value ?? {},
320
+ txnId,
321
+ )
322
+ : this.delete(mutation.table, mutation.key!, txnId);
323
+ if (!change) {
324
+ throw new AppletValidationError(
325
+ `Row "${mutation.key}" is not in "${mutation.table}"`,
326
+ );
327
+ }
328
+ return change;
329
+ });
330
+ }
331
+
332
+ private log(change: AppletChangeV1, txnId?: string): AppletChangeV1 {
333
+ this.sql.exec(
334
+ `INSERT INTO ${quoteIdentifier(CHANGE_TABLE)} ("txnId", "tbl", "op", "rowKey", "row", "at") VALUES (?, ?, ?, ?, ?, ?)`,
335
+ txnId ?? null,
336
+ change.table,
337
+ change.op,
338
+ change.key,
339
+ change.row === undefined ? null : JSON.stringify(change.row),
340
+ new Date().toISOString(),
341
+ );
342
+ this.cursor = Number(
343
+ this.sql
344
+ .exec(`SELECT MAX("seq") AS seq FROM ${quoteIdentifier(CHANGE_TABLE)}`)
345
+ .toArray()[0]!.seq,
346
+ );
347
+ this.trim();
348
+ return change;
349
+ }
350
+
351
+ private trim(): void {
352
+ if (this.cursor <= CHANGE_LOG_LIMIT) return;
353
+ this.sql.exec(
354
+ `DELETE FROM ${quoteIdentifier(CHANGE_TABLE)} WHERE "seq" <= ?`,
355
+ this.cursor - CHANGE_LOG_LIMIT,
356
+ );
357
+ }
358
+
359
+ /** The whole state, table by table, in declaration order. */
360
+ snapshot(): Record<string, Array<Record<string, unknown>>> {
361
+ const tables: Record<string, Array<Record<string, unknown>>> = {};
362
+ for (const name of Object.keys(this.tables))
363
+ tables[name] = this.select(name);
364
+ return tables;
365
+ }
366
+
367
+ /**
368
+ * Changes after `cursor`, or `undefined` when the log no longer reaches back
369
+ * that far and the client must take a full snapshot instead.
370
+ */
371
+ changesSince(cursor: number): AppletChangeV1[] | undefined {
372
+ if (cursor > this.cursor) return undefined;
373
+ if (cursor === this.cursor) return [];
374
+ const oldest = Number(
375
+ this.sql
376
+ .exec(`SELECT MIN("seq") AS seq FROM ${quoteIdentifier(CHANGE_TABLE)}`)
377
+ .toArray()[0]?.seq ?? 0,
378
+ );
379
+ if (oldest === 0 || oldest > cursor + 1) return undefined;
380
+ return this.sql
381
+ .exec(
382
+ `SELECT "tbl", "op", "rowKey", "row" FROM ${quoteIdentifier(CHANGE_TABLE)} WHERE "seq" > ? ORDER BY "seq" ASC`,
383
+ cursor,
384
+ )
385
+ .toArray()
386
+ .map((entry) => {
387
+ const change: AppletChangeV1 = {
388
+ table: String(entry.tbl),
389
+ op: String(entry.op) as AppletChangeV1["op"],
390
+ key: String(entry.rowKey),
391
+ };
392
+ if (entry.row !== null && entry.row !== undefined) {
393
+ change.row = JSON.parse(String(entry.row)) as Record<string, unknown>;
394
+ }
395
+ return change;
396
+ });
397
+ }
398
+ }
@@ -0,0 +1,37 @@
1
+ # __APPLET_NAME__
2
+
3
+ A FrockBot Applet. Two files are yours:
4
+
5
+ | File | What it owns |
6
+ | ----------- | -------------------------------------------------------------------------- |
7
+ | `server.ts` | the tables (state that survives every code change) and the tools Bots call |
8
+ | `ui.tsx` | the page the User sees beside the conversation |
9
+
10
+ ## The loop
11
+
12
+ ```sh
13
+ applet check # type-check and lint; every problem prints as path:line:col message
14
+ applet build # dist/server.js, dist/ui.html, dist/manifest.json
15
+ applet dev # serves the built Applet; prints a URL, opens nothing
16
+ ```
17
+
18
+ Open the printed URL in the Computer's browser to look at it. Publish with
19
+ `applet_publish` once `applet check` is clean.
20
+
21
+ ## Rules the linter enforces
22
+
23
+ - Colours come from the nine `--frockbot-*` theme tokens. The kit's components
24
+ already use them; never write `#hex`, `rgb(...)`, or a colour name.
25
+ - No `fetch`, `XMLHttpRequest`, or `WebSocket`. The Applet has no outbound
26
+ network: reach the world through a tool on the server.
27
+ - Import only from `@frockbot/applet-sdk/*`, `react`, and your own files.
28
+ - Declare tables with `table({ ... })` and tools with `this.tool({ ... }, fn)`.
29
+
30
+ ## Changing the schema
31
+
32
+ Add a column with `.default(...)` or `.optional()` and the SDK adds it on the
33
+ next mount, keeping the rows. For anything else — renaming, rewriting values —
34
+ override `migrate(from)` on the class; it runs once, before the Applet serves
35
+ anything, and throwing fails the mount back to the last known-good generation.
36
+
37
+ The kit's components and their props are documented in the Applets Skill.
@@ -0,0 +1,5 @@
1
+ {
2
+ "id": "__APPLET_ID__",
3
+ "displayName": "__APPLET_NAME__",
4
+ "contract": 1
5
+ }
@@ -0,0 +1,46 @@
1
+ import { Applet, t, table } from "@frockbot/applet-sdk/server";
2
+
3
+ /**
4
+ * The schema. It becomes the SQLite tables, the wire format, and the client's
5
+ * collections, and it survives every publish: changing this file's code never
6
+ * clears the rows.
7
+ */
8
+ const tables = {
9
+ todos: table({
10
+ id: t.id(),
11
+ title: t.text(),
12
+ done: t.boolean().default(false),
13
+ createdAt: t.timestamp(),
14
+ }),
15
+ };
16
+
17
+ /**
18
+ * The server half of __APPLET_NAME__.
19
+ *
20
+ * `tools` is what every Bot of this User can call. `this.db` is the only way
21
+ * to read or write, and each call is atomic.
22
+ */
23
+ export default class TodoApplet extends Applet<typeof tables> {
24
+ tables = tables;
25
+
26
+ tools = {
27
+ add_todo: this.tool(
28
+ { description: "Add a todo to the list", input: { title: t.text() } },
29
+ ({ title }) => {
30
+ this.db.todos.insert({ title, createdAt: new Date().toISOString() });
31
+ return `Added "${title}".`;
32
+ },
33
+ ),
34
+ list_todos: this.tool(
35
+ { description: "List the todos, open ones first", input: {} },
36
+ () => {
37
+ const todos = this.db.todos.select();
38
+ if (todos.length === 0) return "The list is empty.";
39
+ return todos
40
+ .sort((left, right) => Number(left.done) - Number(right.done))
41
+ .map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.title}`)
42
+ .join("\n");
43
+ },
44
+ ),
45
+ };
46
+ }
@@ -0,0 +1,113 @@
1
+ import { useState } from "react";
2
+ import { createApplet, mount, newId } from "@frockbot/applet-sdk/client";
3
+ import {
4
+ Button,
5
+ Checkbox,
6
+ EmptyState,
7
+ Input,
8
+ List,
9
+ ListItem,
10
+ Stack,
11
+ Text,
12
+ Toolbar,
13
+ } from "@frockbot/applet-sdk/kit";
14
+
15
+ import type TodoApplet from "./server";
16
+
17
+ /**
18
+ * The page half of __APPLET_NAME__.
19
+ *
20
+ * `createApplet` connects when the host sends its `init` message, so there is
21
+ * no loading wiring to write. Every mutation is optimistic: the row appears at
22
+ * once and rolls back on its own if the server rejects it.
23
+ */
24
+ const applet = createApplet<TodoApplet>();
25
+
26
+ function App() {
27
+ const [draft, setDraft] = useState("");
28
+ const { status } = applet.useApplet();
29
+ const { data: todos } = applet.useLiveQuery((query) =>
30
+ query
31
+ .from({ todo: applet.tables.todos })
32
+ .orderBy(({ todo }) => todo.createdAt),
33
+ );
34
+
35
+ const add = () => {
36
+ const title = draft.trim();
37
+ if (title === "") return;
38
+ applet.tables.todos.insert({
39
+ id: newId(),
40
+ title,
41
+ done: false,
42
+ createdAt: new Date().toISOString(),
43
+ });
44
+ setDraft("");
45
+ };
46
+
47
+ return (
48
+ <Stack root gap="large">
49
+ <Toolbar
50
+ end={
51
+ <Text size="small" tone="muted">
52
+ {status}
53
+ </Text>
54
+ }
55
+ >
56
+ <Text size="title">__APPLET_NAME__</Text>
57
+ </Toolbar>
58
+
59
+ <Stack direction="row" gap="small" align="end">
60
+ <Input
61
+ label="New todo"
62
+ value={draft}
63
+ placeholder="Buy milk"
64
+ onValueChange={setDraft}
65
+ onKeyDown={(event) => {
66
+ if (event.key === "Enter") add();
67
+ }}
68
+ />
69
+ <Button variant="primary" onClick={add}>
70
+ Add
71
+ </Button>
72
+ </Stack>
73
+
74
+ {todos.length === 0 ? (
75
+ <EmptyState
76
+ title="Nothing yet"
77
+ description="Add your first todo above."
78
+ />
79
+ ) : (
80
+ <List>
81
+ {todos.map((todo) => (
82
+ <ListItem
83
+ key={todo.id}
84
+ start={
85
+ <Checkbox
86
+ checked={todo.done}
87
+ ariaLabel={`Mark "${todo.title}" done`}
88
+ onChange={(done) =>
89
+ applet.tables.todos.update(todo.id, (draftRow) => {
90
+ draftRow.done = done;
91
+ })
92
+ }
93
+ />
94
+ }
95
+ end={
96
+ <Button
97
+ variant="ghost"
98
+ onClick={() => applet.tables.todos.delete(todo.id)}
99
+ >
100
+ Delete
101
+ </Button>
102
+ }
103
+ >
104
+ <Text tone={todo.done ? "muted" : "default"}>{todo.title}</Text>
105
+ </ListItem>
106
+ ))}
107
+ </List>
108
+ )}
109
+ </Stack>
110
+ );
111
+ }
112
+
113
+ mount(<App />);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The only part of the Cloudflare programming model the SDK names.
3
+ *
4
+ * ADR 0022 records the ceiling deliberately: an Applet is a Durable Object with
5
+ * alarms and hibernating sockets, and no adapter hides that. What this file
6
+ * does is keep the surface to the handful of members `server/` actually uses,
7
+ * so an Applet author never types a binding name and the SDK type-checks
8
+ * without `@cloudflare/workers-types` in scope. It lives outside `src/` so a
9
+ * consumer that already has the real Workers types cannot see two declarations
10
+ * of the same module.
11
+ */
12
+
13
+ declare module "cloudflare:workers" {
14
+ export interface AppletSqlCursor {
15
+ toArray(): Array<Record<string, unknown>>;
16
+ }
17
+
18
+ export interface AppletSqlStorageHandle {
19
+ exec(query: string, ...bindings: unknown[]): AppletSqlCursor;
20
+ }
21
+
22
+ export interface AppletDurableObjectStorage {
23
+ readonly sql: AppletSqlStorageHandle;
24
+ transactionSync<T>(closure: () => T): T;
25
+ deleteAll(): Promise<void>;
26
+ }
27
+
28
+ export interface AppletHibernatableWebSocket {
29
+ send(message: string): void;
30
+ close(code?: number, reason?: string): void;
31
+ serializeAttachment(value: unknown): void;
32
+ deserializeAttachment(): unknown;
33
+ }
34
+
35
+ export interface AppletDurableObjectState {
36
+ readonly id: { toString(): string; readonly name?: string };
37
+ readonly storage: AppletDurableObjectStorage;
38
+ acceptWebSocket(socket: AppletHibernatableWebSocket, tags?: string[]): void;
39
+ getWebSockets(tag?: string): AppletHibernatableWebSocket[];
40
+ blockConcurrencyWhile<T>(closure: () => Promise<T>): Promise<T>;
41
+ }
42
+
43
+ export class DurableObject<Env = unknown> {
44
+ constructor(ctx: AppletDurableObjectState, env: Env);
45
+ protected readonly ctx: AppletDurableObjectState;
46
+ protected readonly env: Env;
47
+ }
48
+ }
49
+
50
+ declare const WebSocketPair: {
51
+ new (): {
52
+ 0: import("cloudflare:workers").AppletHibernatableWebSocket;
53
+ 1: import("cloudflare:workers").AppletHibernatableWebSocket;
54
+ };
55
+ };