eraspace 0.1.0 → 0.2.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.
- package/README.md +25 -7
- package/bin.js +105 -54
- package/package.json +2 -4
- package/sdk/src/client/client.ts +26 -11
- package/sdk/src/client/config.ts +6 -4
- package/sdk/src/client/connection.ts +166 -38
- package/sdk/src/client/errors.ts +10 -1
- package/sdk/src/client/files.ts +364 -0
- package/sdk/src/client/index.ts +4 -3
- package/sdk/src/client/pagination.ts +6 -3
- package/sdk/src/client/request-key.ts +13 -0
- package/sdk/src/client/retry.ts +5 -0
- package/sdk/src/client/room.ts +70 -18
- package/sdk/src/client/router.ts +11 -2
- package/sdk/src/client/storage.ts +96 -10
- package/sdk/src/client/subscription.ts +4 -0
- package/sdk/src/client/transport.ts +356 -0
- package/sdk/src/client/types.ts +7 -0
- package/sdk/src/server/index.ts +3 -4
- package/templates/chat/client/index.tsx +9 -5
- package/templates/chat/client/styles.css +15 -0
- package/templates/chat/server/index.ts +16 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# eraspace
|
|
2
2
|
|
|
3
|
-
Build and deploy realtime apps written in TypeScript. The server runs
|
|
3
|
+
Build and deploy realtime apps written in TypeScript. The server runs in V8 with Postgres storage, live queries
|
|
4
4
|
and WebSockets; the client is Preact. Apps need no `package.json`, `tsconfig.json` or `node_modules`: the CLI bundles
|
|
5
5
|
the SDK, Preact and TypeScript.
|
|
6
6
|
|
|
@@ -23,16 +23,34 @@ sign in and claim it (`eraspace auth login`, then `eraspace claim`).
|
|
|
23
23
|
| --- | --- |
|
|
24
24
|
| `new <name>` / `init` | Create an app in a new directory or the current one |
|
|
25
25
|
| `typecheck` | Typecheck `server/`, `client/` and `shared/` |
|
|
26
|
-
| `deploy` | Build the client, compile the server and deploy |
|
|
27
|
-
| `dev` | Deploy, then redeploy
|
|
28
|
-
| `call <fn> '<json>'...` | Call a query, mutation or action |
|
|
29
|
-
| `logs` | Read handler logs (`--follow` to stream) |
|
|
26
|
+
| `deploy` | Build the client, compile the server and deploy (`--delete-table <table>=<rows>` confirms deleting a removed table) |
|
|
27
|
+
| `dev` | Deploy, then redeploy whenever `server/`, `client/`, `shared/` or `.env.eraspace.server` change |
|
|
28
|
+
| `call <fn> '<json>'...` | Call a query, mutation or action, each argument a JSON value |
|
|
29
|
+
| `logs [app]` | Read handler logs (`--follow` to stream) |
|
|
30
30
|
| `inspect` | Show the schema, functions and deploys |
|
|
31
|
-
| `auth login` / `
|
|
31
|
+
| `auth login` / `logout` / `whoami` | Sign in with your browser, sign out, or show who you are |
|
|
32
|
+
| `claim` | Take ownership of this checkout's unclaimed app |
|
|
33
|
+
| `apps` | List your apps |
|
|
34
|
+
| `names` / `names add` / `names rm` | List, add or remove the app's names |
|
|
35
|
+
| `inspection public\|private [app]` | Open or close inspection and logs to everyone |
|
|
36
|
+
| `archive` / `restore [app]` | Stop serving an app and keep its data, or serve it again |
|
|
37
|
+
| `terminate [app] --yes` | Stop an app permanently: its data is kept but it accepts no more deploys |
|
|
38
|
+
| `token create` / `list` / `revoke` | Manage API tokens for one app, or for all your apps with `--personal` |
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
Commands act on the app bound to the current checkout. Pass `--app <name or id>` or set `ERASPACE_APP` to choose
|
|
41
|
+
another. `eraspace --help` and `eraspace <command> --help` list every flag.
|
|
42
|
+
|
|
43
|
+
## CI
|
|
44
|
+
|
|
45
|
+
Create a token with `eraspace token create` (add `--read` for inspection and logs only), then set `ERASPACE_TOKEN` and
|
|
46
|
+
`ERASPACE_TOKEN_API=<platform origin>` in CI. The CLI never sends the token to any other origin.
|
|
34
47
|
|
|
35
48
|
## Platform
|
|
36
49
|
|
|
37
50
|
The CLI deploys to the hosted eraspace platform by default. Set `ERASPACE_URL` or pass `--url` to use another one, such
|
|
38
51
|
as a local platform at `http://localhost:4000`.
|
|
52
|
+
|
|
53
|
+
## Upgrading from 0.1
|
|
54
|
+
|
|
55
|
+
0.2 targets the V8 platform and its JSON socket protocol. Apps deployed with 0.1 are not carried over: deploy them
|
|
56
|
+
afresh with 0.2, which also rebuilds their browser bundles against the new SDK.
|
package/bin.js
CHANGED
|
@@ -15424,12 +15424,12 @@ var posixImpl = /* @__PURE__ */ make21({
|
|
|
15424
15424
|
resolve,
|
|
15425
15425
|
normalize(path) {
|
|
15426
15426
|
if (path.length === 0) return ".";
|
|
15427
|
-
const
|
|
15427
|
+
const isAbsolute2 = path.charCodeAt(0) === 47;
|
|
15428
15428
|
const trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
|
15429
|
-
path = normalizeStringPosix(path, !
|
|
15430
|
-
if (path.length === 0 && !
|
|
15429
|
+
path = normalizeStringPosix(path, !isAbsolute2);
|
|
15430
|
+
if (path.length === 0 && !isAbsolute2) path = ".";
|
|
15431
15431
|
if (path.length > 0 && trailingSeparator) path += "/";
|
|
15432
|
-
if (
|
|
15432
|
+
if (isAbsolute2) return "/" + path;
|
|
15433
15433
|
return path;
|
|
15434
15434
|
},
|
|
15435
15435
|
isAbsolute(path) {
|
|
@@ -15649,9 +15649,9 @@ var posixImpl = /* @__PURE__ */ make21({
|
|
|
15649
15649
|
};
|
|
15650
15650
|
if (path.length === 0) return ret;
|
|
15651
15651
|
let code = path.charCodeAt(0);
|
|
15652
|
-
const
|
|
15652
|
+
const isAbsolute2 = code === 47;
|
|
15653
15653
|
let start;
|
|
15654
|
-
if (
|
|
15654
|
+
if (isAbsolute2) {
|
|
15655
15655
|
ret.root = "/";
|
|
15656
15656
|
start = 1;
|
|
15657
15657
|
} else {
|
|
@@ -15687,11 +15687,11 @@ var posixImpl = /* @__PURE__ */ make21({
|
|
|
15687
15687
|
preDotState === 0 || // The (right-most) trimmed path component is exactly '..'
|
|
15688
15688
|
preDotState === 1 && startDot === end3 - 1 && startDot === startPart + 1) {
|
|
15689
15689
|
if (end3 !== -1) {
|
|
15690
|
-
if (startPart === 0 &&
|
|
15690
|
+
if (startPart === 0 && isAbsolute2) ret.base = ret.name = path.slice(1, end3);
|
|
15691
15691
|
else ret.base = ret.name = path.slice(startPart, end3);
|
|
15692
15692
|
}
|
|
15693
15693
|
} else {
|
|
15694
|
-
if (startPart === 0 &&
|
|
15694
|
+
if (startPart === 0 && isAbsolute2) {
|
|
15695
15695
|
ret.name = path.slice(1, startDot);
|
|
15696
15696
|
ret.base = path.slice(1, end3);
|
|
15697
15697
|
} else {
|
|
@@ -15701,7 +15701,7 @@ var posixImpl = /* @__PURE__ */ make21({
|
|
|
15701
15701
|
ret.ext = path.slice(startDot, end3);
|
|
15702
15702
|
}
|
|
15703
15703
|
if (startPart > 0) ret.dir = path.slice(0, startPart - 1);
|
|
15704
|
-
else if (
|
|
15704
|
+
else if (isAbsolute2) ret.dir = "/";
|
|
15705
15705
|
return ret;
|
|
15706
15706
|
},
|
|
15707
15707
|
sep: "/",
|
|
@@ -38939,7 +38939,7 @@ var runWith2 = (command2, config2) => {
|
|
|
38939
38939
|
// cli/package.json
|
|
38940
38940
|
var package_default = {
|
|
38941
38941
|
name: "eraspace-cli",
|
|
38942
|
-
version: "0.
|
|
38942
|
+
version: "0.2.0",
|
|
38943
38943
|
private: true,
|
|
38944
38944
|
license: "MIT",
|
|
38945
38945
|
type: "module",
|
|
@@ -39468,7 +39468,7 @@ var whoami = Effect_exports.fnUntraced(function* (config2) {
|
|
|
39468
39468
|
import { basename } from "node:path";
|
|
39469
39469
|
|
|
39470
39470
|
// cli/src/build.ts
|
|
39471
|
-
import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
|
|
39471
|
+
import { dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
|
|
39472
39472
|
import * as esbuild from "esbuild";
|
|
39473
39473
|
import { parseSync } from "oxc-parser";
|
|
39474
39474
|
var toPosix = (p) => p.split(sep).join("/");
|
|
@@ -39478,14 +39478,12 @@ function scanImports(file2, source) {
|
|
|
39478
39478
|
const fatal = result4.errors.find((e) => e.severity === "Error");
|
|
39479
39479
|
if (fatal) return { imports: [], error: { message: fatal.message, at: fatal.labels[0]?.start ?? 0 } };
|
|
39480
39480
|
const imports = [];
|
|
39481
|
-
for (const
|
|
39482
|
-
if (
|
|
39483
|
-
|
|
39484
|
-
|
|
39485
|
-
|
|
39486
|
-
|
|
39487
|
-
const first = from[0]?.moduleRequest;
|
|
39488
|
-
if (first) imports.push({ spec: first.value, at: first.start, kind: "import-statement" });
|
|
39481
|
+
for (const statement of result4.program.body) {
|
|
39482
|
+
if (statement.type === "ImportDeclaration" && statement.importKind !== "type") {
|
|
39483
|
+
imports.push({ spec: statement.source.value, at: statement.source.start, kind: "import-statement" });
|
|
39484
|
+
} else if ((statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") && statement.exportKind !== "type" && statement.source) {
|
|
39485
|
+
imports.push({ spec: statement.source.value, at: statement.source.start, kind: "import-statement" });
|
|
39486
|
+
}
|
|
39489
39487
|
}
|
|
39490
39488
|
for (const imp of result4.module.dynamicImports) {
|
|
39491
39489
|
const literal = /^\s*(["'`])(.*)\1\s*$/s.exec(source.slice(imp.moduleRequest.start, imp.moduleRequest.end));
|
|
@@ -39493,10 +39491,19 @@ function scanImports(file2, source) {
|
|
|
39493
39491
|
}
|
|
39494
39492
|
return { imports: imports.sort((a, b) => (a.at ?? 0) - (b.at ?? 0)) };
|
|
39495
39493
|
}
|
|
39494
|
+
var EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".tsx"];
|
|
39495
|
+
function importCandidates(base) {
|
|
39496
|
+
const stem = /\.m?js$/.test(base) ? base.replace(/\.m?js$/, "") : void 0;
|
|
39497
|
+
return [
|
|
39498
|
+
base,
|
|
39499
|
+
...EXTENSIONS.map((ext) => base + ext),
|
|
39500
|
+
...stem === void 0 ? [] : [".ts", ".mts", ".tsx"].map((ext) => stem + ext),
|
|
39501
|
+
...EXTENSIONS.map((ext) => join5(base, "index" + ext))
|
|
39502
|
+
].filter((candidate) => EXTENSIONS.some((ext) => candidate.endsWith(ext)));
|
|
39503
|
+
}
|
|
39496
39504
|
var resolveRelative = Effect_exports.fnUntraced(function* (fromFile, spec) {
|
|
39497
39505
|
const fs = yield* FileSystem_exports.FileSystem;
|
|
39498
|
-
const
|
|
39499
|
-
for (const candidate of [base, base + ".ts", join5(base, "index.ts")]) {
|
|
39506
|
+
for (const candidate of importCandidates(resolve8(dirname2(fromFile), spec))) {
|
|
39500
39507
|
const info2 = yield* fs.stat(candidate).pipe(Effect_exports.option);
|
|
39501
39508
|
if (info2._tag === "Some" && info2.value.type === "File") return candidate;
|
|
39502
39509
|
}
|
|
@@ -39573,6 +39580,7 @@ function clientPlugin(root, sdk, rejected) {
|
|
|
39573
39580
|
setup(build3) {
|
|
39574
39581
|
build3.onResolve({ filter: /^[^./]/ }, async (args2) => {
|
|
39575
39582
|
if (args2.pluginData === "eraspace-sdk") return void 0;
|
|
39583
|
+
if (args2.kind === "entry-point" || isAbsolute(args2.path)) return void 0;
|
|
39576
39584
|
const spec = args2.path;
|
|
39577
39585
|
const fromCapsule = !args2.importer || inCapsule(args2.importer);
|
|
39578
39586
|
if (fromCapsule) {
|
|
@@ -39778,6 +39786,14 @@ var requireTarget = Effect_exports.fnUntraced(function* (config2, explicit) {
|
|
|
39778
39786
|
);
|
|
39779
39787
|
return { ref, claim: claim2 };
|
|
39780
39788
|
});
|
|
39789
|
+
var confirmedDeletes = Effect_exports.fnUntraced(function* (tables) {
|
|
39790
|
+
const out = {};
|
|
39791
|
+
for (const [table2, rows] of Object.entries(tables ?? {})) {
|
|
39792
|
+
if (!/^\d+$/.test(rows)) return yield* fail12(`--delete-table ${table2}=${rows}: the row count must be a whole number, e.g. --delete-table ${table2}=12`);
|
|
39793
|
+
out[table2] = Number(rows);
|
|
39794
|
+
}
|
|
39795
|
+
return out;
|
|
39796
|
+
});
|
|
39781
39797
|
var withoutEnv = Effect_exports.fnUntraced(function* (payload) {
|
|
39782
39798
|
if (Object.keys(payload.env).length) {
|
|
39783
39799
|
yield* Console_exports.warn(
|
|
@@ -39792,19 +39808,10 @@ var okDeployed = ok(Deployed);
|
|
|
39792
39808
|
var requestedName = (config2, opts) => opts.name !== void 0 ? { name: opts.name } : { name: config2.name ?? toAppName(basename(config2.root)), uniqueName: true };
|
|
39793
39809
|
var createApp = Effect_exports.fnUntraced(function* (config2, payload, extra, opts, signedIn) {
|
|
39794
39810
|
const naming = requestedName(config2, opts);
|
|
39795
|
-
const
|
|
39796
|
-
body: { ...payload, ...extra, ...
|
|
39811
|
+
const response = yield* api(config2, "POST", "/_api/apps", {
|
|
39812
|
+
body: { ...payload, ...extra, ...naming },
|
|
39797
39813
|
timeout: DEPLOY_TIMEOUT
|
|
39798
39814
|
});
|
|
39799
|
-
let response = yield* create(naming);
|
|
39800
|
-
if (response.status === 401 && !signedIn && naming.uniqueName) {
|
|
39801
|
-
yield* Console_exports.warn(
|
|
39802
|
-
c.yellow(
|
|
39803
|
-
`This platform names apps only for signed-in developers, so this app gets an id. Run eraspace auth login and eraspace claim to name it ${naming.name}.`
|
|
39804
|
-
)
|
|
39805
|
-
);
|
|
39806
|
-
response = yield* create({});
|
|
39807
|
-
}
|
|
39808
39815
|
if (response.status === 409 && !naming.uniqueName) {
|
|
39809
39816
|
return yield* fail12(
|
|
39810
39817
|
signedIn ? `The name ${naming.name} is taken. Pick another with --name or in eraspace.json; if it is your app, deploy to it with --app ${naming.name}.` : `The name ${naming.name} is taken. Pick another with --name.`
|
|
@@ -39827,7 +39834,11 @@ var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
|
|
|
39827
39834
|
const build3 = yield* buildCapsule(config2.root);
|
|
39828
39835
|
if (!build3.ok || !build3.payload)
|
|
39829
39836
|
return yield* fail12(yield* formatErrors(build3.errors, config2.root));
|
|
39830
|
-
const
|
|
39837
|
+
const deleteTables = yield* confirmedDeletes(opts.deleteTables);
|
|
39838
|
+
const extra = {
|
|
39839
|
+
...opts.publicInspect ? { publicInspection: true } : {},
|
|
39840
|
+
...Object.keys(deleteTables).length ? { deleteTables } : {}
|
|
39841
|
+
};
|
|
39831
39842
|
const signedIn = (yield* bearerFor(config2.origin)) !== void 0;
|
|
39832
39843
|
const t = yield* target(config2);
|
|
39833
39844
|
let result4;
|
|
@@ -40134,7 +40145,7 @@ import { basename as basename2, join as join7, relative as relative3, resolve as
|
|
|
40134
40145
|
var TEMPLATES = ["chat", "todo", "poll"];
|
|
40135
40146
|
var agents = (cli) => `# eraspace capsule: agent instructions
|
|
40136
40147
|
|
|
40137
|
-
This directory is one eraspace app ("capsule"): a TypeScript server
|
|
40148
|
+
This directory is one eraspace app ("capsule"): a TypeScript server running in V8, a Preact client, a
|
|
40138
40149
|
Postgres-backed database with live queries, and hosting, all through the \`eraspace\` CLI. Treat this directory as the
|
|
40139
40150
|
whole app. Build the requested app by editing the starter files, deploy it, verify it, and report the URL.
|
|
40140
40151
|
|
|
@@ -40217,7 +40228,9 @@ export default capsule({
|
|
|
40217
40228
|
- \`ctx.log.info(msg, data?)\` (also debug/warn/error) writes to \`eraspace logs\`.
|
|
40218
40229
|
- Endpoints receive \`req\` with \`method, path, url, headers.get(), query.get(), json(), text()\` and return
|
|
40219
40230
|
\`json(value)\`, \`text(string)\`, \`empty()\`, \`redirect(url)\` or a plain value (sent as JSON).
|
|
40220
|
-
- Removing a table
|
|
40231
|
+
- Removing a populated table needs its row count confirmed: \`${cli} deploy --delete-table <table>=<rows>\` (the
|
|
40232
|
+
refused deploy prints the exact flag). Deletion runs in background batches.
|
|
40233
|
+
- Changing a field's type or making it required fails the deploy while stored rows do not match; migrate them first.
|
|
40221
40234
|
|
|
40222
40235
|
## Client API (\`eraspace/client\`)
|
|
40223
40236
|
|
|
@@ -40251,28 +40264,55 @@ export default function App() {
|
|
|
40251
40264
|
- Realtime rooms, not stored: \`const room = client.useRoom(name, meta)\` gives \`room.presence\`,
|
|
40252
40265
|
\`room.broadcast(event, payload)\` and \`room.on(event, cb)\` (call \`on\` inside \`useEffect\` and return its
|
|
40253
40266
|
cleanup). \`client.usePresence(name, meta)\` returns just the members.
|
|
40254
|
-
- Files:
|
|
40255
|
-
Up to 5 MiB per file.
|
|
40267
|
+
- Files: see "File uploads" below.
|
|
40256
40268
|
- Pages: \`Router\`, \`Route\`, \`Link\`, \`useLocation\`, \`useNavigate\`, \`useParams\` from \`eraspace/client\`.
|
|
40257
40269
|
- Client imports are limited to \`eraspace/client\`, \`preact\`, \`preact/hooks\`, \`@preact/signals\` and relative files.
|
|
40258
40270
|
|
|
40259
|
-
##
|
|
40271
|
+
## File uploads
|
|
40260
40272
|
|
|
40261
|
-
|
|
40262
|
-
destructuring, spread, template literals, Map, Set, JSON, Math, Date, URL, RegExp, fetch) with these limits:
|
|
40273
|
+
Files go to the app's bucket, not the database. Save the returned \`FileRef\` in a row, then preview it.
|
|
40263
40274
|
|
|
40264
|
-
|
|
40265
|
-
|
|
40266
|
-
|
|
40267
|
-
|
|
40268
|
-
|
|
40269
|
-
|
|
40270
|
-
|
|
40271
|
-
|
|
40272
|
-
|
|
40273
|
-
|
|
40274
|
-
|
|
40275
|
-
|
|
40275
|
+
\`\`\`tsx
|
|
40276
|
+
// server/index.ts: declare the file in the schema and copy its fields in the mutation (extra fields are refused).
|
|
40277
|
+
photos: table({ caption: string(), file: object({ key: string(), name: string(), contentType: string(), size: number() }) }),
|
|
40278
|
+
addPhoto: mutation(async (ctx, caption: string, file: { key: string; name: string; contentType: string; size: number }) => {
|
|
40279
|
+
if (!/^[0-9a-f-]{36}$/.test(String(file?.key))) throw new Error("Upload the photo first.");
|
|
40280
|
+
return ctx.db.photos.insert({ caption, file: { key: file.key, name: file.name, contentType: file.contentType, size: file.size } });
|
|
40281
|
+
}),
|
|
40282
|
+
|
|
40283
|
+
// client/index.tsx
|
|
40284
|
+
import { createClient, FileUpload, FilePreview } from "eraspace/client";
|
|
40285
|
+
const addPhoto = client.useMutation("addPhoto");
|
|
40286
|
+
<FileUpload client={client} accept="image/*" onUpload={(file) => addPhoto(caption, file)} />
|
|
40287
|
+
{photos?.map((p) => <FilePreview key={p.id} client={client} file={p.file} />)}
|
|
40288
|
+
\`\`\`
|
|
40289
|
+
|
|
40290
|
+
- \`<FileUpload>\` is a drop zone and file picker with progress and error text. Props: \`accept\`, \`multiple\`,
|
|
40291
|
+
\`maxBytes\`, \`disabled\`, \`class\` (replaces its default style; \`data-dragging\` is set during a drag) and
|
|
40292
|
+
children for the label. \`onUpload(file)\` runs once per stored file; if it throws, the file is deleted again.
|
|
40293
|
+
- \`<FilePreview file={fileRef or key}>\` shows images, video and audio inline, plain text in a frame, and other
|
|
40294
|
+
types (PDF, zip...) as a download card. \`caption={false}\` hides the name/size line; \`class\` replaces its style.
|
|
40295
|
+
Pass the saved \`FileRef\`: a bare key costs a metadata request per preview.
|
|
40296
|
+
- Custom UI: \`const { upload, uploading, progress, error } = client.useUpload({ accept, maxBytes, onUpload })\`,
|
|
40297
|
+
then \`upload(input.files)\`. Lower level: \`client.storage.upload(blob, { onProgress })\`, \`.url(key)\`,
|
|
40298
|
+
\`.info(key)\` and \`.delete(key)\`.
|
|
40299
|
+
- \`accept\` and \`maxBytes\` are checked in the browser only; the server stores any type up to its limit.
|
|
40300
|
+
- Up to 5 MiB per file and 100 MiB per app. Anyone with a key can read or delete that file; never upload secrets.
|
|
40301
|
+
|
|
40302
|
+
## Server execution
|
|
40303
|
+
|
|
40304
|
+
Server TypeScript is transformed with oxc and runs as ES modules in V8. JavaScript language features such as
|
|
40305
|
+
classes, closures, async/await, generators, getters, private fields, Proxy, Reflect and BigInt use V8 semantics.
|
|
40306
|
+
Values crossing the database or RPC boundary must be JSON-compatible.
|
|
40307
|
+
|
|
40308
|
+
- Imports support relative deployed files and \`eraspace/server\`. npm packages, Node built-ins and dynamic
|
|
40309
|
+
\`import()\` are unavailable. Apps need no installed server dependencies.
|
|
40310
|
+
- The host supplies \`fetch\`, \`URL\`, \`Headers\`, \`Request\`, \`Response\`, \`crypto.randomUUID\`,
|
|
40311
|
+
\`TextEncoder\`, \`TextDecoder\`, timers and \`console\`.
|
|
40312
|
+
- Persist state in the database. Module state is not durable and resets when an app unloads or a worker restarts.
|
|
40313
|
+
- Await database calls. Mutations run through one writer per app and reply after their Postgres commit.
|
|
40314
|
+
- Handlers have CPU, wall-clock and heap limits. Keep loops and reads bounded.
|
|
40315
|
+
- Outbound fetch is available to claimed apps in actions and read-only endpoints. Private addresses are blocked.
|
|
40276
40316
|
|
|
40277
40317
|
## Deploys without an account
|
|
40278
40318
|
|
|
@@ -40410,6 +40450,12 @@ var deployFlags = {
|
|
|
40410
40450
|
publicInspect: Flag_exports.Boolean("public-inspect").pipe(Flag_exports.withDefault(false), Flag_exports.withDescription("Let anyone read this app's inspection data and logs"))
|
|
40411
40451
|
};
|
|
40412
40452
|
var deployOptions = (input) => ({ name: value3(input.name), publicInspect: input.publicInspect });
|
|
40453
|
+
var deleteTableFlag = {
|
|
40454
|
+
deleteTable: Flag_exports.KeyValuePair("delete-table").pipe(
|
|
40455
|
+
Flag_exports.withDescription("Delete a table removed from the schema, confirming its current row count: --delete-table <table>=<rows> (repeatable)"),
|
|
40456
|
+
Flag_exports.optional
|
|
40457
|
+
)
|
|
40458
|
+
};
|
|
40413
40459
|
var browserFlag = {
|
|
40414
40460
|
browser: Flag_exports.Boolean("browser").pipe(Flag_exports.withDefault(true), Flag_exports.withDescription("Open the sign-in page in a browser; --no-browser only prints the URL"))
|
|
40415
40461
|
};
|
|
@@ -40431,8 +40477,13 @@ var initCommand = Command_exports.make("init", { template: templateFlag }, ({ te
|
|
|
40431
40477
|
var typecheckCommand = Command_exports.make("typecheck", {}, () => Effect_exports.flatMap(config, (c2) => typecheck(c2.root))).pipe(
|
|
40432
40478
|
Command_exports.withDescription("Typecheck server/, client/ and shared/ (no tsconfig.json or node_modules needed)")
|
|
40433
40479
|
);
|
|
40434
|
-
var deployCommand = Command_exports.make(
|
|
40435
|
-
|
|
40480
|
+
var deployCommand = Command_exports.make(
|
|
40481
|
+
"deploy",
|
|
40482
|
+
{ ...deployFlags, ...deleteTableFlag },
|
|
40483
|
+
(input) => Effect_exports.flatMap(config, (c2) => deploy(c2, { ...deployOptions(input), deleteTables: value3(input.deleteTable) }))
|
|
40484
|
+
).pipe(
|
|
40485
|
+
Command_exports.withDescription("Build and deploy. Without a login this creates a temporary unclaimed app."),
|
|
40486
|
+
Command_exports.withExamples([{ command: "eraspace deploy --delete-table drafts=12", description: "Deploy a schema without drafts, deleting its 12 rows" }])
|
|
40436
40487
|
);
|
|
40437
40488
|
var devCommand = Command_exports.make("dev", deployFlags, (input) => Effect_exports.flatMap(config, (c2) => dev(c2, deployOptions(input)))).pipe(
|
|
40438
40489
|
Command_exports.withDescription("Deploy, then redeploy whenever server/, client/, shared/ or .env.eraspace.server change")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eraspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "eraspace: build, deploy and manage eraspace capsules",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"cli",
|
|
9
9
|
"realtime",
|
|
10
10
|
"preact",
|
|
11
|
-
"
|
|
11
|
+
"v8"
|
|
12
12
|
],
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"bin": {
|
|
@@ -24,10 +24,8 @@
|
|
|
24
24
|
"@preact/signals": "2.11.3",
|
|
25
25
|
"@tailwindcss/node": "4.3.3",
|
|
26
26
|
"@tailwindcss/oxide": "4.3.3",
|
|
27
|
-
"@types/phoenix": "1.6.7",
|
|
28
27
|
"esbuild": "0.28.2",
|
|
29
28
|
"oxc-parser": "0.154.0",
|
|
30
|
-
"phoenix": "1.8.15",
|
|
31
29
|
"preact": "11.0.1",
|
|
32
30
|
"preact-iso": "2.12.2",
|
|
33
31
|
"tailwindcss": "4.3.3",
|
package/sdk/src/client/client.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { computed, type ReadonlySignal } from "@preact/signals";
|
|
2
|
-
import { useEffect, useMemo, useRef } from "preact/hooks";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
3
3
|
import { createStorageClient } from "./storage.ts";
|
|
4
|
+
import { useUpload, type UseUploadOptions } from "./files.ts";
|
|
4
5
|
import type { ClientOptions } from "./config.ts";
|
|
5
6
|
import { subKey, stableStringify } from "./sub-key.ts";
|
|
6
7
|
import type { Subscription } from "./subscription.ts";
|
|
7
8
|
import type { RoomConn } from "./room.ts";
|
|
8
9
|
import type { EraspaceClient, Room } from "./types.ts";
|
|
10
|
+
import { EraspaceError } from "./errors.ts";
|
|
9
11
|
import { ClientImpl } from "./connection.ts";
|
|
10
12
|
import { Pager } from "./pagination.ts";
|
|
11
13
|
|
|
@@ -39,7 +41,8 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
39
41
|
return useMemo(() => computed(() => {
|
|
40
42
|
const err = sub.error.value;
|
|
41
43
|
const value: unknown = sub.value.value;
|
|
42
|
-
|
|
44
|
+
// Retried failures keep showing the last value; a gone app cannot recover.
|
|
45
|
+
if (err && (value === undefined || err.name === "AppGone")) throw err;
|
|
43
46
|
return value;
|
|
44
47
|
}), [sub]);
|
|
45
48
|
}
|
|
@@ -61,7 +64,7 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
function useAction(name: string) {
|
|
64
|
-
return useMemo(() => (...args: unknown[]) => get().call(name, args), [name]);
|
|
67
|
+
return useMemo(() => (...args: unknown[]) => get().call(name, args, "action"), [name]);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function usePaginatedQuery(name: string, args: unknown[], opts?: { initialNumItems?: number }) {
|
|
@@ -85,8 +88,17 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
85
88
|
|
|
86
89
|
function useRoomConn(room: string, meta: Record<string, unknown> | undefined): RoomConn {
|
|
87
90
|
const c = get();
|
|
88
|
-
const
|
|
89
|
-
|
|
91
|
+
const [generation, renew] = useState(0);
|
|
92
|
+
// Acquiring during render holds nothing: only a committed effect retains the room.
|
|
93
|
+
const r = useMemo(() => c.acquireRoom(room), [room, generation]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!c.retainRoom(r)) {
|
|
96
|
+
// Dropped between render and commit.
|
|
97
|
+
renew((n) => n + 1);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
return () => c.releaseRoom(r);
|
|
101
|
+
}, [r]);
|
|
90
102
|
const metaKey = meta === undefined ? undefined : stableStringify(meta);
|
|
91
103
|
useEffect(() => {
|
|
92
104
|
if (meta !== undefined) r.setMeta(meta);
|
|
@@ -104,8 +116,9 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
104
116
|
presence: r.members.value,
|
|
105
117
|
self: r.self.value,
|
|
106
118
|
state: r.state.value,
|
|
119
|
+
error: r.error.value,
|
|
107
120
|
broadcast: (event: string, payload?: unknown) => {
|
|
108
|
-
r.channel.push("broadcast", { event, payload: payload ?? null });
|
|
121
|
+
void r.channel.push("broadcast", { event, payload: payload ?? null }).catch(error => { r.error.value = EraspaceError.from(error); });
|
|
109
122
|
},
|
|
110
123
|
on: (event: string, cb: (payload: any, from: string | undefined) => void) => {
|
|
111
124
|
let set = r.listeners.get(event);
|
|
@@ -114,14 +127,15 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
114
127
|
return () => set!.delete(cb);
|
|
115
128
|
},
|
|
116
129
|
setState: (key: string, value: unknown) => {
|
|
117
|
-
r.
|
|
118
|
-
r.channel.push("state:set", { key, value: value ?? null });
|
|
130
|
+
r.setState(key, value);
|
|
119
131
|
},
|
|
120
132
|
};
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
const storage = createStorageClient(() => get().base);
|
|
123
136
|
const client = {
|
|
124
|
-
storage
|
|
137
|
+
storage,
|
|
138
|
+
useUpload: (opts?: UseUploadOptions) => useUpload(storage, opts),
|
|
125
139
|
useQuery,
|
|
126
140
|
useQuerySignal,
|
|
127
141
|
useQuerySelector,
|
|
@@ -131,8 +145,9 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
131
145
|
usePresence,
|
|
132
146
|
useRoom,
|
|
133
147
|
query: (name: string, ...args: unknown[]) => get().call(name, args),
|
|
134
|
-
mutation: (name: string, ...args: unknown[]) => get().
|
|
135
|
-
|
|
148
|
+
mutation: (name: string, ...args: unknown[]) => get().mutate(name, args),
|
|
149
|
+
retryMutation: (name: string, args: unknown[], requestKey: string) => get().retryMutation(name, args, requestKey),
|
|
150
|
+
action: (name: string, ...args: unknown[]) => get().call(name, args, "action"),
|
|
136
151
|
subscribe: (name: string, args: unknown[], onValue: Fn, onError?: Fn) => get().subscribe(name, args, onValue, onError),
|
|
137
152
|
get app() {
|
|
138
153
|
return get().app;
|
package/sdk/src/client/config.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SocketOptions, WebSocketConstructor } from "./transport.ts";
|
|
2
|
+
|
|
1
3
|
/** `window.__ERASPACE__`, injected by the platform's HTML shell (SPEC §6). */
|
|
2
4
|
export interface EraspaceConfig {
|
|
3
5
|
app: string;
|
|
@@ -12,7 +14,7 @@ export interface ClientOptions {
|
|
|
12
14
|
socket?: string;
|
|
13
15
|
base?: string;
|
|
14
16
|
version?: number;
|
|
15
|
-
/** Reload the page when the app is redeployed
|
|
17
|
+
/** Reload the page when the app is redeployed. Default `false`. */
|
|
16
18
|
reloadOnDeploy?: boolean;
|
|
17
19
|
/** Push timeout in ms. Default 15000. */
|
|
18
20
|
timeout?: number;
|
|
@@ -21,9 +23,9 @@ export interface ClientOptions {
|
|
|
21
23
|
/** Delay before refreshing a subscription still waiting for a mutation commit. Default 2000 ms. */
|
|
22
24
|
optimisticTimeoutMs?: number;
|
|
23
25
|
/** WebSocket constructor (tests, non-browser runtimes). */
|
|
24
|
-
transport?:
|
|
25
|
-
/**
|
|
26
|
-
socketOptions?:
|
|
26
|
+
transport?: WebSocketConstructor;
|
|
27
|
+
/** Reconnect and rejoin delay overrides. */
|
|
28
|
+
socketOptions?: Pick<SocketOptions, "reconnectAfterMs" | "rejoinAfterMs">;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
declare global {
|