eraspace 0.1.0 → 0.3.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/bin.js CHANGED
@@ -241,6 +241,34 @@ __export(Effect_exports, {
241
241
  zipWith: () => zipWith3
242
242
  });
243
243
 
244
+ // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Context.js
245
+ var Context_exports = {};
246
+ __export(Context_exports, {
247
+ Reference: () => Reference,
248
+ Service: () => Service,
249
+ ServiceTypeId: () => ServiceTypeId,
250
+ add: () => add,
251
+ addOrOmit: () => addOrOmit,
252
+ addUnsafe: () => addUnsafe,
253
+ empty: () => empty,
254
+ get: () => get,
255
+ getOption: () => getOption,
256
+ getOrElse: () => getOrElse2,
257
+ getOrUndefined: () => getOrUndefined2,
258
+ getOrUndefinedUnsafe: () => getOrUndefinedUnsafe,
259
+ getUnsafe: () => getUnsafe,
260
+ hasSameCache: () => hasSameCache,
261
+ isContext: () => isContext,
262
+ isKey: () => isKey,
263
+ isReference: () => isReference,
264
+ make: () => make5,
265
+ makeUnsafe: () => makeUnsafe,
266
+ merge: () => merge,
267
+ mergeAll: () => mergeAll,
268
+ omit: () => omit,
269
+ pick: () => pick
270
+ });
271
+
244
272
  // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Pipeable.js
245
273
  var pipeArguments = (self, args2) => {
246
274
  switch (args2.length) {
@@ -2273,6 +2301,7 @@ var Proto = {
2273
2301
  };
2274
2302
  var hasSameCache = (self, that) => self.cacheRoot === that.cacheRoot;
2275
2303
  var isContext = (u) => hasProperty(u, TypeId3);
2304
+ var isKey = (u) => hasProperty(u, ServiceTypeId);
2276
2305
  var isReference = (u) => !!u[ReferenceTypeId];
2277
2306
  var empty = () => emptyContext2;
2278
2307
  var emptyContext2 = /* @__PURE__ */ makeUnsafe(/* @__PURE__ */ new Map());
@@ -2293,6 +2322,12 @@ var addUnsafe = (self, key, service4) => {
2293
2322
  parent: impl.overlay
2294
2323
  }, impl.depth + 1);
2295
2324
  };
2325
+ var addOrOmit = /* @__PURE__ */ dual(3, (self, key, service4) => service4._tag === "None" ? omit(key)(self) : add(self, key, service4.value));
2326
+ var getOrElse2 = /* @__PURE__ */ dual(3, (self, key, orElse6) => {
2327
+ const value4 = lookup(self, key.key);
2328
+ if (value4 !== notFound) return value4;
2329
+ return isReference(key) ? getDefaultValue(key) : orElse6();
2330
+ });
2296
2331
  var getOrUndefined2 = /* @__PURE__ */ dual(2, (self, key) => getOrUndefinedUnsafe(self, key.key));
2297
2332
  var getOrUndefinedUnsafe = (self, key) => {
2298
2333
  const value4 = lookup(self, key);
@@ -2342,6 +2377,26 @@ var mergeAll = (...ctxs) => {
2342
2377
  }
2343
2378
  return makeUnsafe(map18);
2344
2379
  };
2380
+ var pick = (...services) => (self) => {
2381
+ if (services.length <= 1) {
2382
+ const map18 = /* @__PURE__ */ new Map();
2383
+ if (services.length === 1) {
2384
+ const key = services[0].key;
2385
+ const value4 = lookup(self, key);
2386
+ if (value4 !== notFound) map18.set(key, value4);
2387
+ }
2388
+ return makeUnsafe(map18);
2389
+ }
2390
+ const keep = new Set(services.map((key) => key.key));
2391
+ return withFlat(self, (map18) => map18.forEach((_, key) => {
2392
+ if (!keep.has(key)) map18.delete(key);
2393
+ }));
2394
+ };
2395
+ var omit = (...keys4) => (self) => withFlat(self, (map18) => {
2396
+ for (let i = 0; i < keys4.length; i++) {
2397
+ map18.delete(keys4[i].key);
2398
+ }
2399
+ });
2345
2400
  var Reference = Service;
2346
2401
 
2347
2402
  // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Duration.js
@@ -2988,7 +3043,7 @@ var merge2 = /* @__PURE__ */ match3({
2988
3043
  onFailure: identity,
2989
3044
  onSuccess: identity
2990
3045
  });
2991
- var getOrElse2 = /* @__PURE__ */ dual(2, (self, onFailure) => isFailure2(self) ? onFailure(self.failure) : self.success);
3046
+ var getOrElse3 = /* @__PURE__ */ dual(2, (self, onFailure) => isFailure2(self) ? onFailure(self.failure) : self.success);
2992
3047
  var getOrThrowWith2 = /* @__PURE__ */ dual(2, (self, onFailure) => {
2993
3048
  if (isSuccess2(self)) {
2994
3049
  return self.success;
@@ -15424,12 +15479,12 @@ var posixImpl = /* @__PURE__ */ make21({
15424
15479
  resolve,
15425
15480
  normalize(path) {
15426
15481
  if (path.length === 0) return ".";
15427
- const isAbsolute = path.charCodeAt(0) === 47;
15482
+ const isAbsolute2 = path.charCodeAt(0) === 47;
15428
15483
  const trailingSeparator = path.charCodeAt(path.length - 1) === 47;
15429
- path = normalizeStringPosix(path, !isAbsolute);
15430
- if (path.length === 0 && !isAbsolute) path = ".";
15484
+ path = normalizeStringPosix(path, !isAbsolute2);
15485
+ if (path.length === 0 && !isAbsolute2) path = ".";
15431
15486
  if (path.length > 0 && trailingSeparator) path += "/";
15432
- if (isAbsolute) return "/" + path;
15487
+ if (isAbsolute2) return "/" + path;
15433
15488
  return path;
15434
15489
  },
15435
15490
  isAbsolute(path) {
@@ -15649,9 +15704,9 @@ var posixImpl = /* @__PURE__ */ make21({
15649
15704
  };
15650
15705
  if (path.length === 0) return ret;
15651
15706
  let code = path.charCodeAt(0);
15652
- const isAbsolute = code === 47;
15707
+ const isAbsolute2 = code === 47;
15653
15708
  let start;
15654
- if (isAbsolute) {
15709
+ if (isAbsolute2) {
15655
15710
  ret.root = "/";
15656
15711
  start = 1;
15657
15712
  } else {
@@ -15687,11 +15742,11 @@ var posixImpl = /* @__PURE__ */ make21({
15687
15742
  preDotState === 0 || // The (right-most) trimmed path component is exactly '..'
15688
15743
  preDotState === 1 && startDot === end3 - 1 && startDot === startPart + 1) {
15689
15744
  if (end3 !== -1) {
15690
- if (startPart === 0 && isAbsolute) ret.base = ret.name = path.slice(1, end3);
15745
+ if (startPart === 0 && isAbsolute2) ret.base = ret.name = path.slice(1, end3);
15691
15746
  else ret.base = ret.name = path.slice(startPart, end3);
15692
15747
  }
15693
15748
  } else {
15694
- if (startPart === 0 && isAbsolute) {
15749
+ if (startPart === 0 && isAbsolute2) {
15695
15750
  ret.name = path.slice(1, startDot);
15696
15751
  ret.base = path.slice(1, end3);
15697
15752
  } else {
@@ -15701,7 +15756,7 @@ var posixImpl = /* @__PURE__ */ make21({
15701
15756
  ret.ext = path.slice(startDot, end3);
15702
15757
  }
15703
15758
  if (startPart > 0) ret.dir = path.slice(0, startPart - 1);
15704
- else if (isAbsolute) ret.dir = "/";
15759
+ else if (isAbsolute2) ret.dir = "/";
15705
15760
  return ret;
15706
15761
  },
15707
15762
  sep: "/",
@@ -16361,7 +16416,7 @@ function transformOptionalEffect(f) {
16361
16416
  transform: f
16362
16417
  });
16363
16418
  }
16364
- function omit() {
16419
+ function omit2() {
16365
16420
  return transformOptional(() => none2());
16366
16421
  }
16367
16422
  function withDefault(defaultValue) {
@@ -18565,9 +18620,9 @@ function isPattern(regExp, annotations) {
18565
18620
  });
18566
18621
  }
18567
18622
  var bodyOwners = /* @__PURE__ */ new WeakMap();
18568
- function copy2(ast, changes2) {
18569
- const out = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, changes2);
18570
- if (Reflect.ownKeys(changes2).every((key) => key === "context" || key === "encoding")) {
18623
+ function copy2(ast, changes3) {
18624
+ const out = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, changes3);
18625
+ if (Reflect.ownKeys(changes3).every((key) => key === "context" || key === "encoding")) {
18571
18626
  bodyOwners.set(out, getContextOwner(ast));
18572
18627
  }
18573
18628
  return out;
@@ -24947,10 +25002,10 @@ function toFormatter(ast, options3) {
24947
25002
  }
24948
25003
 
24949
25004
  // node_modules/.pnpm/effect@4.0.5/node_modules/effect/dist/Struct.js
24950
- var pick = /* @__PURE__ */ dual(2, (self, keys4) => {
25005
+ var pick2 = /* @__PURE__ */ dual(2, (self, keys4) => {
24951
25006
  return buildStruct(self, (k, v) => hasPropertyKey(keys4, k) ? [k, v] : void 0);
24952
25007
  });
24953
- var omit2 = /* @__PURE__ */ dual(2, (self, keys4) => {
25008
+ var omit3 = /* @__PURE__ */ dual(2, (self, keys4) => {
24954
25009
  return buildStruct(self, (k, v) => !hasPropertyKey(keys4, k) ? [k, v] : void 0);
24955
25010
  });
24956
25011
  var assign = /* @__PURE__ */ dual(2, (self, that) => {
@@ -25070,10 +25125,10 @@ var OptionalImpl = class {
25070
25125
  this.replaceResult = replaceResult;
25071
25126
  }
25072
25127
  replace(a, s) {
25073
- return getOrElse2(this.replaceResult(a, s), () => s);
25128
+ return getOrElse3(this.replaceResult(a, s), () => s);
25074
25129
  }
25075
25130
  modify(f) {
25076
- return (s) => getOrElse2(flatMap2(this.getResult(s), (a) => this.replaceResult(f(a), s)), () => s);
25131
+ return (s) => getOrElse3(flatMap2(this.getResult(s), (a) => this.replaceResult(f(a), s)), () => s);
25077
25132
  }
25078
25133
  compose(that) {
25079
25134
  return make36(compose3(this.node, that.node));
@@ -25121,14 +25176,14 @@ var OptionalImpl = class {
25121
25176
  })));
25122
25177
  }
25123
25178
  pick(keys4) {
25124
- return this.compose(makeLens(pick(keys4), (p, a) => ({
25125
- ...omit2(a, keys4),
25179
+ return this.compose(makeLens(pick2(keys4), (p, a) => ({
25180
+ ...omit3(a, keys4),
25126
25181
  ...p
25127
25182
  })));
25128
25183
  }
25129
25184
  omit(keys4) {
25130
- return this.compose(makeLens(omit2(keys4), (o, a) => ({
25131
- ...pick(a, keys4),
25185
+ return this.compose(makeLens(omit3(keys4), (o, a) => ({
25186
+ ...pick2(a, keys4),
25132
25187
  ...o
25133
25188
  })));
25134
25189
  }
@@ -25174,7 +25229,7 @@ var OptionalImpl = class {
25174
25229
  );
25175
25230
  }
25176
25231
  modifyAll(f) {
25177
- return (s) => getOrElse2(flatMap2(this.getResult(s), (as5) => this.replaceResult(as5.map(f), s)), () => s);
25232
+ return (s) => getOrElse3(flatMap2(this.getResult(s), (as5) => this.replaceResult(as5.map(f), s)), () => s);
25178
25233
  }
25179
25234
  };
25180
25235
  var IsoImpl = class extends OptionalImpl {
@@ -25213,7 +25268,7 @@ var PrismImpl = class extends OptionalImpl {
25213
25268
  return this.set(a);
25214
25269
  }
25215
25270
  modify(f) {
25216
- return (s) => getOrElse2(map2(this.getResult(s), (a) => this.set(f(a))), () => s);
25271
+ return (s) => getOrElse3(map2(this.getResult(s), (a) => this.set(f(a))), () => s);
25217
25272
  }
25218
25273
  };
25219
25274
  function make36(node) {
@@ -26001,7 +26056,7 @@ function toIssueEffect(self) {
26001
26056
  return catchCause3(self, (cause) => failCauseSync2(() => map6(cause, (error2) => error2.issue)));
26002
26057
  }
26003
26058
  function withDecodingDefaultKey(defaultValue, options3) {
26004
- const encode5 = options3?.encodingStrategy === "omit" ? omit() : passthrough2();
26059
+ const encode5 = options3?.encodingStrategy === "omit" ? omit2() : passthrough2();
26005
26060
  return (self) => {
26006
26061
  return optionalKey2(toEncoded2(self)).pipe(decodeTo2(self, {
26007
26062
  decode: withDefault(toIssueEffect(defaultValue)),
@@ -26015,7 +26070,7 @@ function withDecodingDefaultTypeKey(defaultValue, options3) {
26015
26070
  };
26016
26071
  }
26017
26072
  function withDecodingDefault(defaultValue, options3) {
26018
- const encode5 = options3?.encodingStrategy === "omit" ? omit() : passthrough2();
26073
+ const encode5 = options3?.encodingStrategy === "omit" ? omit2() : passthrough2();
26019
26074
  return (self) => {
26020
26075
  return optional2(toEncoded2(self)).pipe(decodeTo2(self, {
26021
26076
  decode: withDefault(toIssueEffect(defaultValue)),
@@ -38939,7 +38994,7 @@ var runWith2 = (command2, config2) => {
38939
38994
  // cli/package.json
38940
38995
  var package_default = {
38941
38996
  name: "eraspace-cli",
38942
- version: "0.1.0",
38997
+ version: "0.3.0",
38943
38998
  private: true,
38944
38999
  license: "MIT",
38945
39000
  type: "module",
@@ -39266,6 +39321,48 @@ function formatLog(l) {
39266
39321
  const data = l.data === void 0 || l.data === null ? "" : " " + c.gray(JSON.stringify(l.data));
39267
39322
  return `${c.gray(time3)} ${color(level.padEnd(5))} ${fn3}${l.msg ?? ""}${data}`;
39268
39323
  }
39324
+ var SKIPPED = {
39325
+ fetch: "uses fetch",
39326
+ identity: "reads the caller's identity",
39327
+ running: "was running",
39328
+ recovering: "cache recovering",
39329
+ "new index": "reads an index this deploy adds",
39330
+ committed: "a commit changed it during the check",
39331
+ budget: "out of time",
39332
+ limit: "too many entries",
39333
+ interrupted: "interrupted"
39334
+ };
39335
+ var call = (fn3, args2) => {
39336
+ const text2 = Array.isArray(args2) ? args2.map((a) => JSON.stringify(a)).join(", ") : JSON.stringify(args2);
39337
+ return `${fn3}(${text2 && text2.length > 80 ? `${text2.slice(0, 77)}...` : text2 ?? ""})`;
39338
+ };
39339
+ function formatShadow(report) {
39340
+ if (report.note) return c.dim(`Live queries: none checked (${report.note})`);
39341
+ const total = report.total ?? 0;
39342
+ if (total === 0) return c.dim("Live queries: none cached, nothing to check");
39343
+ const errors = report.errors ?? 0;
39344
+ const changed = report.changed ?? 0;
39345
+ const parts2 = [
39346
+ `${report.unchanged ?? 0} unchanged`,
39347
+ changed ? c.yellow(`${changed} change value`) : "0 change value",
39348
+ errors ? c.red(`${errors} now throw`) : "0 now throw",
39349
+ ...report.fixed ? [c.green(`${report.fixed} fixed`)] : [],
39350
+ ...report.skipped ? [c.dim(`${report.skipped} skipped`)] : []
39351
+ ];
39352
+ const at = report.commitSeq ? ` at commit ${report.commitSeq}` : "";
39353
+ const lines2 = [`Live queries: ${report.checked ?? 0} of ${total} checked against the new code${at}: ${parts2.join(", ")}`];
39354
+ for (const e of report.entries ?? []) {
39355
+ const what = call(e.fn, e.args);
39356
+ if (e.outcome === "error") lines2.push(` ${c.red("throws ")} ${what}: ${e.error ? `${e.error.name}: ${e.error.message}` : ""}`);
39357
+ else if (e.outcome === "changed") lines2.push(` ${c.yellow("changes")} ${what}${e.error ? `: ${e.error.name}: ${e.error.message}` : ""}`);
39358
+ else if (e.outcome === "fixed") lines2.push(` ${c.green("fixed ")} ${what}`);
39359
+ else lines2.push(c.dim(` skipped ${what}: ${SKIPPED[e.reason ?? ""] ?? e.reason ?? ""}`));
39360
+ }
39361
+ const listed = report.entries?.length ?? 0;
39362
+ const notable = errors + changed + (report.fixed ?? 0) + (report.skipped ?? 0);
39363
+ if (notable > listed) lines2.push(c.dim(` ... and ${notable - listed} more`));
39364
+ return lines2.join("\n");
39365
+ }
39269
39366
 
39270
39367
  // cli/src/api.ts
39271
39368
  var Timestamp = Schema_exports.NullOr(Schema_exports.Union([Schema_exports.Number, Schema_exports.String]));
@@ -39280,10 +39377,34 @@ var App = Schema_exports.Struct({
39280
39377
  expiresAt: Schema_exports.optional(Timestamp),
39281
39378
  publicInspection: Schema_exports.optional(Schema_exports.Boolean)
39282
39379
  });
39380
+ var Count = Schema_exports.optional(Schema_exports.Number);
39381
+ var ShadowReport = Schema_exports.Struct({
39382
+ total: Count,
39383
+ checked: Count,
39384
+ unchanged: Count,
39385
+ changed: Count,
39386
+ errors: Count,
39387
+ fixed: Count,
39388
+ skipped: Count,
39389
+ commitSeq: Count,
39390
+ note: Schema_exports.optional(Schema_exports.String),
39391
+ entries: Schema_exports.optional(
39392
+ Schema_exports.Array(
39393
+ Schema_exports.Struct({
39394
+ fn: Schema_exports.String,
39395
+ args: Schema_exports.optional(Schema_exports.Unknown),
39396
+ outcome: Schema_exports.String,
39397
+ reason: Schema_exports.optional(Schema_exports.String),
39398
+ error: Schema_exports.optional(Schema_exports.Struct({ name: Schema_exports.String, message: Schema_exports.String }))
39399
+ })
39400
+ )
39401
+ )
39402
+ });
39283
39403
  var Deployed = Schema_exports.Struct({
39284
39404
  ...App.fields,
39285
39405
  version: Schema_exports.optional(Schema_exports.Number),
39286
- claimToken: Schema_exports.optional(Schema_exports.String)
39406
+ claimToken: Schema_exports.optional(Schema_exports.String),
39407
+ shadow: Schema_exports.optional(ShadowReport)
39287
39408
  });
39288
39409
  var AppList = Schema_exports.Struct({ apps: Schema_exports.Array(App) });
39289
39410
  var StateChange = Schema_exports.Struct({ state: Schema_exports.String });
@@ -39448,15 +39569,20 @@ var login = Effect_exports.fnUntraced(function* (config2, browser) {
39448
39569
  }
39449
39570
  return yield* fail12("The sign-in code expired. Run eraspace auth login again.");
39450
39571
  });
39572
+ var LogoutResult = Schema_exports.Struct({ ok: Schema_exports.Literal(true) });
39451
39573
  var logout = Effect_exports.fnUntraced(function* (config2) {
39452
39574
  const session = yield* sessionFor(config2.origin);
39453
39575
  if (!session) {
39454
39576
  yield* Console_exports.log(`Not signed in to ${config2.origin}.`);
39455
39577
  return;
39456
39578
  }
39457
- yield* request3(config2, "POST", "/_api/auth/logout", { bearer: Redacted_exports.make(session.token) }).pipe(Effect_exports.timeout("10 seconds"), Effect_exports.ignore);
39579
+ const response = yield* api(config2, "POST", "/_api/auth/logout", { bearer: Redacted_exports.make(session.token), timeout: "10 seconds" });
39580
+ if (response.status !== 401) {
39581
+ if (response.status >= 400 || response.json.ok === false) return yield* Effect_exports.fail(yield* failure(response));
39582
+ yield* decode5(LogoutResult, response.json, "sign-out");
39583
+ }
39458
39584
  yield* removeSession(config2.origin);
39459
- yield* Console_exports.log(`Signed out of ${config2.origin}.`);
39585
+ yield* Console_exports.log(response.status === 401 ? `Removed invalid session for ${config2.origin}.` : `Signed out of ${config2.origin}.`);
39460
39586
  });
39461
39587
  var okWhoami = ok(Whoami);
39462
39588
  var whoami = Effect_exports.fnUntraced(function* (config2) {
@@ -39468,7 +39594,7 @@ var whoami = Effect_exports.fnUntraced(function* (config2) {
39468
39594
  import { basename } from "node:path";
39469
39595
 
39470
39596
  // cli/src/build.ts
39471
- import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
39597
+ import { dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
39472
39598
  import * as esbuild from "esbuild";
39473
39599
  import { parseSync } from "oxc-parser";
39474
39600
  var toPosix = (p) => p.split(sep).join("/");
@@ -39478,14 +39604,12 @@ function scanImports(file2, source) {
39478
39604
  const fatal = result4.errors.find((e) => e.severity === "Error");
39479
39605
  if (fatal) return { imports: [], error: { message: fatal.message, at: fatal.labels[0]?.start ?? 0 } };
39480
39606
  const imports = [];
39481
- for (const imp of result4.module.staticImports) {
39482
- if (imp.entries.length && imp.entries.every((e) => e.isType)) continue;
39483
- imports.push({ spec: imp.moduleRequest.value, at: imp.moduleRequest.start, kind: "import-statement" });
39484
- }
39485
- for (const exp of result4.module.staticExports) {
39486
- const from = exp.entries.filter((e) => e.moduleRequest && !e.isType);
39487
- const first = from[0]?.moduleRequest;
39488
- if (first) imports.push({ spec: first.value, at: first.start, kind: "import-statement" });
39607
+ for (const statement of result4.program.body) {
39608
+ if (statement.type === "ImportDeclaration" && statement.importKind !== "type") {
39609
+ imports.push({ spec: statement.source.value, at: statement.source.start, kind: "import-statement" });
39610
+ } else if ((statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") && statement.exportKind !== "type" && statement.source) {
39611
+ imports.push({ spec: statement.source.value, at: statement.source.start, kind: "import-statement" });
39612
+ }
39489
39613
  }
39490
39614
  for (const imp of result4.module.dynamicImports) {
39491
39615
  const literal = /^\s*(["'`])(.*)\1\s*$/s.exec(source.slice(imp.moduleRequest.start, imp.moduleRequest.end));
@@ -39493,10 +39617,20 @@ function scanImports(file2, source) {
39493
39617
  }
39494
39618
  return { imports: imports.sort((a, b) => (a.at ?? 0) - (b.at ?? 0)) };
39495
39619
  }
39620
+ var MAX_MODULE_BYTES = 256 * 1024;
39621
+ var EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".tsx"];
39622
+ function importCandidates(base) {
39623
+ const stem = /\.m?js$/.test(base) ? base.replace(/\.m?js$/, "") : void 0;
39624
+ return [
39625
+ base,
39626
+ ...EXTENSIONS.map((ext) => base + ext),
39627
+ ...stem === void 0 ? [] : [".ts", ".mts", ".tsx"].map((ext) => stem + ext),
39628
+ ...EXTENSIONS.map((ext) => join5(base, "index" + ext))
39629
+ ].filter((candidate) => EXTENSIONS.some((ext) => candidate.endsWith(ext)));
39630
+ }
39496
39631
  var resolveRelative = Effect_exports.fnUntraced(function* (fromFile, spec) {
39497
39632
  const fs = yield* FileSystem_exports.FileSystem;
39498
- const base = resolve8(dirname2(fromFile), spec);
39499
- for (const candidate of [base, base + ".ts", join5(base, "index.ts")]) {
39633
+ for (const candidate of importCandidates(resolve8(dirname2(fromFile), spec))) {
39500
39634
  const info2 = yield* fs.stat(candidate).pipe(Effect_exports.option);
39501
39635
  if (info2._tag === "Some" && info2.value.type === "File") return candidate;
39502
39636
  }
@@ -39519,6 +39653,10 @@ var collectServerFiles = Effect_exports.fnUntraced(function* (root) {
39519
39653
  const rel = toPosix(relative2(root, abs2));
39520
39654
  const source = yield* fs.readFileString(abs2);
39521
39655
  files[rel] = source;
39656
+ const bytes2 = Buffer.byteLength(source, "utf8");
39657
+ if (bytes2 > MAX_MODULE_BYTES) {
39658
+ errors.push({ file: rel, line: 0, column: 0, message: `module is ${bytes2} bytes, over the server's ${MAX_MODULE_BYTES} byte (256 KiB) limit; split it into smaller modules` });
39659
+ }
39522
39660
  const { imports, error: error2 } = scanImports(abs2, source);
39523
39661
  if (error2) {
39524
39662
  errors.push({ file: rel, ...lineCol(source, error2.at), message: error2.message });
@@ -39573,6 +39711,7 @@ function clientPlugin(root, sdk, rejected) {
39573
39711
  setup(build3) {
39574
39712
  build3.onResolve({ filter: /^[^./]/ }, async (args2) => {
39575
39713
  if (args2.pluginData === "eraspace-sdk") return void 0;
39714
+ if (args2.kind === "entry-point" || isAbsolute(args2.path)) return void 0;
39576
39715
  const spec = args2.path;
39577
39716
  const fromCapsule = !args2.importer || inCapsule(args2.importer);
39578
39717
  if (fromCapsule) {
@@ -39778,6 +39917,14 @@ var requireTarget = Effect_exports.fnUntraced(function* (config2, explicit) {
39778
39917
  );
39779
39918
  return { ref, claim: claim2 };
39780
39919
  });
39920
+ var confirmedDeletes = Effect_exports.fnUntraced(function* (tables) {
39921
+ const out = {};
39922
+ for (const [table2, rows] of Object.entries(tables ?? {})) {
39923
+ 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`);
39924
+ out[table2] = Number(rows);
39925
+ }
39926
+ return out;
39927
+ });
39781
39928
  var withoutEnv = Effect_exports.fnUntraced(function* (payload) {
39782
39929
  if (Object.keys(payload.env).length) {
39783
39930
  yield* Console_exports.warn(
@@ -39788,23 +39935,13 @@ var withoutEnv = Effect_exports.fnUntraced(function* (payload) {
39788
39935
  }
39789
39936
  return { ...payload, env: {} };
39790
39937
  });
39791
- var okDeployed = ok(Deployed);
39792
39938
  var requestedName = (config2, opts) => opts.name !== void 0 ? { name: opts.name } : { name: config2.name ?? toAppName(basename(config2.root)), uniqueName: true };
39793
39939
  var createApp = Effect_exports.fnUntraced(function* (config2, payload, extra, opts, signedIn) {
39794
39940
  const naming = requestedName(config2, opts);
39795
- const create = (body) => api(config2, "POST", "/_api/apps", {
39796
- body: { ...payload, ...extra, ...body },
39941
+ const response = yield* api(config2, "POST", "/_api/apps", {
39942
+ body: { ...payload, ...extra, ...naming },
39797
39943
  timeout: DEPLOY_TIMEOUT
39798
39944
  });
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
39945
  if (response.status === 409 && !naming.uniqueName) {
39809
39946
  return yield* fail12(
39810
39947
  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,22 +39964,30 @@ var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
39827
39964
  const build3 = yield* buildCapsule(config2.root);
39828
39965
  if (!build3.ok || !build3.payload)
39829
39966
  return yield* fail12(yield* formatErrors(build3.errors, config2.root));
39830
- const extra = opts.publicInspect ? { publicInspection: true } : {};
39967
+ const deleteTables = yield* confirmedDeletes(opts.deleteTables);
39968
+ const extra = {
39969
+ ...opts.publicInspect ? { publicInspection: true } : {},
39970
+ ...Object.keys(deleteTables).length ? { deleteTables } : {},
39971
+ ...opts.shadow ? { shadow: true } : {},
39972
+ ...opts.failOnQueryErrors ? { failOnQueryErrors: true } : {}
39973
+ };
39831
39974
  const signedIn = (yield* bearerFor(config2.origin)) !== void 0;
39832
39975
  const t = yield* target(config2);
39833
39976
  let result4;
39834
39977
  if (t.ref) {
39835
39978
  const payload = t.claim ? yield* withoutEnv(build3.payload) : build3.payload;
39836
- result4 = yield* okDeployed(
39837
- config2,
39838
- "POST",
39839
- `/_api/apps/${enc(t.ref)}/deploy`,
39840
- {
39841
- body: { ...payload, ...extra },
39842
- claim: t.claim,
39843
- timeout: DEPLOY_TIMEOUT
39844
- }
39845
- );
39979
+ const path = `/_api/apps/${enc(t.ref)}/deploy`;
39980
+ const response = yield* api(config2, "POST", path, {
39981
+ body: { ...payload, ...extra },
39982
+ claim: t.claim,
39983
+ timeout: DEPLOY_TIMEOUT
39984
+ });
39985
+ if (response.status >= 400 || response.json.ok === false) {
39986
+ const refused = Schema_exports.decodeUnknownOption(ShadowReport)(response.json.shadow);
39987
+ if (Option_exports.isSome(refused)) yield* Console_exports.log(formatShadow(refused.value));
39988
+ return yield* Effect_exports.fail(yield* failure(response, config2.root));
39989
+ }
39990
+ result4 = yield* decode5(Deployed, response.json, `POST ${path}`);
39846
39991
  if (t.claim && result4.claimed)
39847
39992
  yield* removeLocalDeploy(config2.root, config2.origin);
39848
39993
  } else if (signedIn) {
@@ -39867,6 +40012,7 @@ var deploy = Effect_exports.fnUntraced(function* (config2, opts = {}) {
39867
40012
  expiresAt: Number.isFinite(expiresAt) ? expiresAt : null
39868
40013
  });
39869
40014
  }
40015
+ if (result4.shadow) yield* Console_exports.log(formatShadow(result4.shadow));
39870
40016
  const label = result4.names?.[0] ?? result4.id;
39871
40017
  yield* Console_exports.log(
39872
40018
  `${label}${result4.version === void 0 ? "" : ` v${result4.version}`}: ${result4.url ?? config2.url}`
@@ -39953,7 +40099,10 @@ var dev = Effect_exports.fnUntraced(function* (config2, opts) {
39953
40099
  });
39954
40100
 
39955
40101
  // cli/src/logs.ts
39956
- var IDLE = Duration_exports.seconds(75);
40102
+ var LogTimeouts = Context_exports.Reference(
40103
+ "eraspace/cli/LogTimeouts",
40104
+ { defaultValue: () => ({ idle: Duration_exports.seconds(75), stalled: Duration_exports.seconds(30), errorBody: Duration_exports.seconds(10) }) }
40105
+ );
39957
40106
  var Printed = class {
39958
40107
  lines = [];
39959
40108
  limit;
@@ -39994,12 +40143,14 @@ var readOnce = Effect_exports.fnUntraced(function* (config2, path, claim2, follo
39994
40143
  const response = yield* request3(config2, "GET", path, { claim: claim2 }).pipe(
39995
40144
  Effect_exports.timeoutOrElse({ duration: "30 seconds", orElse: () => Effect_exports.fail(new Transient({ message: `The platform at ${config2.origin} did not answer within 30s.` })) })
39996
40145
  );
40146
+ const timeouts = yield* LogTimeouts;
40147
+ const errorText = response.text.pipe(Effect_exports.timeout(timeouts.errorBody), Effect_exports.orElseSucceed(() => ""));
39997
40148
  if (isGatewayError(response.status)) {
39998
- yield* response.text.pipe(Effect_exports.ignore);
40149
+ yield* errorText;
39999
40150
  return yield* new Transient({ message: `The platform at ${config2.origin} is unavailable (HTTP ${response.status}).` });
40000
40151
  }
40001
40152
  if (response.status >= 400) {
40002
- const text2 = yield* response.text.pipe(Effect_exports.orElseSucceed(() => ""));
40153
+ const text2 = yield* errorText;
40003
40154
  let json2 = { ok: false, error: text2.trim() || `HTTP ${response.status}` };
40004
40155
  try {
40005
40156
  json2 = JSON.parse(text2);
@@ -40007,12 +40158,15 @@ var readOnce = Effect_exports.fnUntraced(function* (config2, path, claim2, follo
40007
40158
  }
40008
40159
  return yield* Effect_exports.fail(yield* failure({ status: response.status, json: json2 }, config2.root));
40009
40160
  }
40010
- let lines2 = response.stream.pipe(
40161
+ const lines2 = response.stream.pipe(
40162
+ Stream_exports.mapError((error2) => new Transient({ message: `The log stream broke: ${describe(error2.cause ?? error2)}` })),
40163
+ Stream_exports.timeoutOrElse({
40164
+ duration: follow ? timeouts.idle : timeouts.stalled,
40165
+ orElse: () => Stream_exports.fail(new Transient({ message: follow ? "The log stream went quiet." : `The platform at ${config2.origin} stopped sending logs.` }))
40166
+ }),
40011
40167
  Stream_exports.decodeText(),
40012
- Stream_exports.splitLines,
40013
- Stream_exports.mapError((error2) => new Transient({ message: `The log stream broke: ${describe(error2.cause ?? error2)}` }))
40168
+ Stream_exports.splitLines
40014
40169
  );
40015
- if (follow) lines2 = lines2.pipe(Stream_exports.timeoutOrElse({ duration: IDLE, orElse: () => Stream_exports.fail(new Transient({ message: "The log stream went quiet." })) }));
40016
40170
  const fresh2 = printed.connection();
40017
40171
  yield* lines2.pipe(
40018
40172
  Stream_exports.filter((line) => line.trim() !== ""),
@@ -40079,6 +40233,68 @@ var lifecycle = Effect_exports.fnUntraced(function* (config2, action, explicit,
40079
40233
  const result4 = yield* okState(config2, "POST", `/_api/apps/${enc(t.ref)}/${action}`, { claim: t.claim });
40080
40234
  yield* Console_exports.log(`${t.ref}: ${result4.state}`);
40081
40235
  });
40236
+ var explain = Effect_exports.fnUntraced(function* (config2, fn3, inputs, json2) {
40237
+ const args2 = yield* parseCallArgs(inputs);
40238
+ const t = yield* requireTarget(config2);
40239
+ const report = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/explain`, { body: { fn: fn3, args: args2 }, claim: t.claim });
40240
+ if (json2) return yield* Console_exports.log(JSON.stringify(report, null, 2));
40241
+ const scans = report.scans ?? [];
40242
+ yield* Console_exports.log(`${fn3}: read ${String(report.rowsRead)} row${report.rowsRead === 1 ? "" : "s"} in ${scans.length} scan${scans.length === 1 ? "" : "s"} and returned ${String(report.returned)}`);
40243
+ for (const s of scans) yield* Console_exports.log(` ${s.table} via ${s.index} (${s.mode}): read ${s.rowsRead}, kept ${s.kept} of ${s.rows}`);
40244
+ const suggestions = report.suggestions ?? [];
40245
+ if (!suggestions.length) yield* Console_exports.log(c.dim("No index suggestions."));
40246
+ for (const s of suggestions) yield* Console_exports.log(`${c.yellow("suggest")} ${s.message}`);
40247
+ });
40248
+ var profile = Effect_exports.fnUntraced(function* (config2, explicit, seconds2, out) {
40249
+ const t = yield* requireTarget(config2, explicit);
40250
+ yield* Console_exports.error(c.dim(`Profiling ${t.ref} for ${seconds2}s...`));
40251
+ const result4 = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/profile`, { body: { seconds: seconds2 }, claim: t.claim, timeout: Duration_exports.seconds(seconds2 + 30) });
40252
+ const file2 = out ?? `${t.ref}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.cpuprofile`;
40253
+ const fs = yield* FileSystem_exports.FileSystem;
40254
+ yield* fs.writeFileString(file2, JSON.stringify(result4));
40255
+ const nodes = new Map((result4.nodes ?? []).map((n) => [n.id, n]));
40256
+ const counts = /* @__PURE__ */ new Map();
40257
+ let busy = 0;
40258
+ for (const id2 of result4.samples ?? []) {
40259
+ const f = nodes.get(id2)?.callFrame;
40260
+ if (!f || f.functionName === "(idle)" || f.functionName === "(root)") continue;
40261
+ busy++;
40262
+ const at = f.url ? ` ${f.url}:${f.lineNumber + 1}` : "";
40263
+ const name = `${f.functionName || "(anonymous)"}${at}`;
40264
+ counts.set(name, (counts.get(name) ?? 0) + 1);
40265
+ }
40266
+ const samples = (result4.samples ?? []).length;
40267
+ yield* Console_exports.log(`${t.ref}: busy in ${busy} of ${samples} samples. Open ${file2} in Chrome DevTools (Performance > Load profile).`);
40268
+ for (const [name, n] of [...counts].sort((a, b) => b[1] - a[1]).slice(0, 10)) {
40269
+ yield* Console_exports.log(` ${(100 * n / Math.max(busy, 1)).toFixed(1).padStart(5)}% ${name}`);
40270
+ }
40271
+ });
40272
+ var changes2 = Effect_exports.fnUntraced(function* (config2, since, limit) {
40273
+ const t = yield* requireTarget(config2);
40274
+ const response = yield* request3(config2, "GET", `/_api/apps/${enc(t.ref)}/changes?since=${since}&limit=${limit}`, { claim: t.claim });
40275
+ const text2 = yield* response.text.pipe(Effect_exports.orElseSucceed(() => ""));
40276
+ if (response.status >= 400) {
40277
+ let json2 = { ok: false, error: text2.trim() || `HTTP ${response.status}` };
40278
+ try {
40279
+ json2 = JSON.parse(text2);
40280
+ } catch {
40281
+ }
40282
+ return yield* Effect_exports.fail(yield* failure({ status: response.status, json: json2 }));
40283
+ }
40284
+ const lines2 = text2.split("\n").filter(Boolean);
40285
+ for (const line of lines2) yield* Console_exports.log(line);
40286
+ const head5 = Number(response.headers["x-eraspace-commit-seq"] ?? 0);
40287
+ const last = lines2.length ? JSON.parse(lines2.at(-1)).seq : since;
40288
+ const more = response.headers["x-eraspace-more"] === "1";
40289
+ yield* Console_exports.error(c.dim(more ? `Latest commit ${head5}; continue with --since ${last}` : `Up to date at commit ${head5}`));
40290
+ });
40291
+ var undo = Effect_exports.fnUntraced(function* (config2, to, yes) {
40292
+ const t = yield* requireTarget(config2);
40293
+ if (!yes) return yield* fail12(`eraspace undo ${to} puts every row of ${t.ref} changed after commit ${to} back as it was. Re-run with --yes.`);
40294
+ const result4 = yield* okJson(config2, "POST", `/_api/apps/${enc(t.ref)}/undo`, { body: { toSeq: to }, claim: t.claim });
40295
+ const undone = Number(result4.undone ?? 0);
40296
+ yield* Console_exports.log(undone ? `${t.ref}: restored ${undone} row${undone === 1 ? "" : "s"} to commit ${to} as commit ${String(result4.commitSeq)}` : `${t.ref}: nothing changed after commit ${to}`);
40297
+ });
40082
40298
  var listTokens = Effect_exports.fnUntraced(function* (config2) {
40083
40299
  const { tokens } = yield* ok(TokenList)(config2, "GET", "/_api/tokens");
40084
40300
  if (!tokens.length) yield* Console_exports.log("No tokens.");
@@ -40111,7 +40327,7 @@ Pass each argument as JSON, for example eraspace call send '"general"' '{"text":
40111
40327
  }
40112
40328
  return Effect_exports.succeed(args2);
40113
40329
  }
40114
- var call = Effect_exports.fnUntraced(function* (config2, fn3, inputs) {
40330
+ var call2 = Effect_exports.fnUntraced(function* (config2, fn3, inputs) {
40115
40331
  const args2 = yield* parseCallArgs(inputs);
40116
40332
  const t = yield* requireTarget(config2);
40117
40333
  const { status, text: text2 } = yield* Effect_exports.gen(function* () {
@@ -40134,7 +40350,7 @@ import { basename as basename2, join as join7, relative as relative3, resolve as
40134
40350
  var TEMPLATES = ["chat", "todo", "poll"];
40135
40351
  var agents = (cli) => `# eraspace capsule: agent instructions
40136
40352
 
40137
- This directory is one eraspace app ("capsule"): a TypeScript server compiled to BEAM bytecode, a Preact client, a
40353
+ This directory is one eraspace app ("capsule"): a TypeScript server running in V8, a Preact client, a
40138
40354
  Postgres-backed database with live queries, and hosting, all through the \`eraspace\` CLI. Treat this directory as the
40139
40355
  whole app. Build the requested app by editing the starter files, deploy it, verify it, and report the URL.
40140
40356
 
@@ -40211,13 +40427,19 @@ export default capsule({
40211
40427
  \`paginate({ cursor, numItems })\`. Every table has an implicit \`by_creation\` index. Await every db call.
40212
40428
  - Queries and actions have a read-only db. Write in mutations or writable endpoints. Actions can call
40213
40429
  \`ctx.runQuery\`, \`ctx.runMutation\` and global \`fetch\`.
40430
+ - Mutations schedule later work with \`ctx.scheduler.runAfter(ms, "mutationName", ...args)\`, which returns a job id.
40431
+ The job commits with the mutation and its run commits once; only mutations can be scheduled.
40214
40432
  - Handler arguments are positional and not validated. Check them in the handler.
40215
40433
  - \`ctx.auth.userId\` is a per-connection guest id that changes on reload. It is not a login: never use it for
40216
40434
  ownership or access control. Every app is public.
40217
40435
  - \`ctx.log.info(msg, data?)\` (also debug/warn/error) writes to \`eraspace logs\`.
40218
40436
  - Endpoints receive \`req\` with \`method, path, url, headers.get(), query.get(), json(), text()\` and return
40219
40437
  \`json(value)\`, \`text(string)\`, \`empty()\`, \`redirect(url)\` or a plain value (sent as JSON).
40220
- - Removing a table that still holds rows is refused on deploy. Keep it in the schema.
40438
+ - Removing a populated table needs its row count confirmed: \`${cli} deploy --delete-table <table>=<rows>\` (the
40439
+ refused deploy prints the exact flag). Deletion runs in background batches.
40440
+ - Changing a field's type or making it required fails the deploy while stored rows do not match. Declare a
40441
+ migration instead: \`table({ ... }).migrate(1, row => ({ ...row, done: false }))\` upgrades old rows when read
40442
+ and rewrites them in the background. Number migrations 1, 2, 3, ... and keep each one until its rows are done.
40221
40443
 
40222
40444
  ## Client API (\`eraspace/client\`)
40223
40445
 
@@ -40226,7 +40448,7 @@ import { createClient } from "eraspace/client";
40226
40448
  import { useState } from "preact/hooks";
40227
40449
  import type app from "../server/index";
40228
40450
 
40229
- const client = createClient<typeof app>({ reloadOnDeploy: true });
40451
+ const client = createClient<typeof app>();
40230
40452
 
40231
40453
  export default function App() {
40232
40454
  const items = client.useQuery("items", "some-list"); // undefined while loading, then live
@@ -40251,28 +40473,59 @@ export default function App() {
40251
40473
  - Realtime rooms, not stored: \`const room = client.useRoom(name, meta)\` gives \`room.presence\`,
40252
40474
  \`room.broadcast(event, payload)\` and \`room.on(event, cb)\` (call \`on\` inside \`useEffect\` and return its
40253
40475
  cleanup). \`client.usePresence(name, meta)\` returns just the members.
40254
- - Files: \`await client.storage.upload(file)\` returns \`{ key }\`; \`client.storage.url(key)\`; \`client.storage.delete(key)\`.
40255
- Up to 5 MiB per file.
40476
+ - Files: see "File uploads" below.
40256
40477
  - Pages: \`Router\`, \`Route\`, \`Link\`, \`useLocation\`, \`useNavigate\`, \`useParams\` from \`eraspace/client\`.
40478
+ - Open tabs reload by themselves after each deploy. Pass \`createClient({ reloadOnDeploy: false })\` to keep the
40479
+ page and react to \`client.onDeployed(version => ...)\` instead.
40257
40480
  - Client imports are limited to \`eraspace/client\`, \`preact\`, \`preact/hooks\`, \`@preact/signals\` and relative files.
40258
40481
 
40259
- ## TypeScript subset on the server
40482
+ ## File uploads
40260
40483
 
40261
- Server code is compiled to BEAM, not run in Node. It supports modern TypeScript (classes, closures, async/await,
40262
- destructuring, spread, template literals, Map, Set, JSON, Math, Date, URL, RegExp, fetch) with these limits:
40484
+ Files go to the app's bucket, not the database. Save the returned \`FileRef\` in a row, then preview it.
40263
40485
 
40264
- - No npm packages, Node built-ins (\`node:*\`, \`fs\`, \`process\`, \`Buffer\`) or dynamic \`import()\`. Do not install
40265
- app dependencies.
40266
- - Rejected at compile time: \`eval\`, \`new Function\`, Proxy, Reflect, generators, getters/setters, private
40267
- \`#fields\`, decorators, enums, namespaces, BigInt, symbols, labeled statements, \`for await\`, static blocks and
40268
- named function expressions (\`const f = function g() {}\`; use arrow functions or function declarations).
40269
- - Do not declare regex literals at module top level; create them inside the function that uses them.
40270
- - Do not use constructor parameter properties (\`constructor(private x: number)\`). Declare the field and assign
40271
- \`this.x = x\` in the constructor.
40272
- - Module-level variables are reset for every handler call. Persist state in the database.
40273
- - \`await\` blocks; there is no real concurrency inside a handler. Handlers time out after about a second and have
40274
- a memory limit, so keep loops and reads bounded.
40275
- - No Intl or \`toLocaleString\` formatting on the server. Format dates and numbers in the client.
40486
+ \`\`\`tsx
40487
+ // server/index.ts: declare the file in the schema and copy its fields in the mutation (extra fields are refused).
40488
+ photos: table({ caption: string(), file: object({ key: string(), name: string(), contentType: string(), size: number() }) }),
40489
+ addPhoto: mutation(async (ctx, caption: string, file: { key: string; name: string; contentType: string; size: number }) => {
40490
+ if (!/^[0-9a-f-]{36}$/.test(String(file?.key))) throw new Error("Upload the photo first.");
40491
+ return ctx.db.photos.insert({ caption, file: { key: file.key, name: file.name, contentType: file.contentType, size: file.size } });
40492
+ }),
40493
+
40494
+ // client/index.tsx
40495
+ import { createClient, FileUpload, FilePreview } from "eraspace/client";
40496
+ const addPhoto = client.useMutation("addPhoto");
40497
+ <FileUpload client={client} accept="image/*" onUpload={(file) => addPhoto(caption, file)} />
40498
+ {photos?.map((p) => <FilePreview key={p.id} client={client} file={p.file} />)}
40499
+ \`\`\`
40500
+
40501
+ - \`<FileUpload>\` is a drop zone and file picker with progress and error text. Props: \`accept\`, \`multiple\`,
40502
+ \`maxBytes\`, \`disabled\`, \`class\` (replaces its default style; \`data-dragging\` is set during a drag) and
40503
+ children for the label. \`onUpload(file)\` runs once per stored file; if it throws, the file is deleted again.
40504
+ - \`<FilePreview file={fileRef or key}>\` shows images, video and audio inline, plain text in a frame, and other
40505
+ types (PDF, zip...) as a download card. \`caption={false}\` hides the name/size line; \`class\` replaces its style.
40506
+ Pass the saved \`FileRef\`: a bare key costs a metadata request per preview. Images, video and audio uploaded
40507
+ from the same page show at once from the browser's copy; \`<FileUpload>\` shows an image while it uploads.
40508
+ - Custom UI: \`const { upload, uploading, progress, error, file } = client.useUpload({ accept, maxBytes, onUpload })\`,
40509
+ then \`upload(input.files)\`; \`file\` is the Blob uploading now. Lower level: \`client.storage.upload(blob, { onProgress })\`,
40510
+ \`.url(key)\`, \`.localUrl(key)\`, \`.info(key)\` and \`.delete(key)\`.
40511
+ - \`accept\` and \`maxBytes\` are checked in the browser only; the server stores any type up to its limit.
40512
+ - Up to 5 MiB per file and 100 MiB per app. Anyone with a key can read or delete that file; never upload secrets.
40513
+
40514
+ ## Server execution
40515
+
40516
+ Server TypeScript is transformed with oxc and runs as ES modules in V8. JavaScript language features such as
40517
+ classes, closures, async/await, generators, getters, private fields, Proxy, Reflect and BigInt use V8 semantics.
40518
+ Values crossing the database or RPC boundary must be JSON-compatible.
40519
+
40520
+ - Imports support relative deployed files and \`eraspace/server\`. npm packages, Node built-ins and dynamic
40521
+ \`import()\` are unavailable. Apps need no installed server dependencies.
40522
+ - The host supplies \`fetch\`, \`URL\`, \`URLPattern\`, \`Headers\`, \`Request\`, \`Response\`, Web Streams,
40523
+ \`Blob\`, \`File\`, \`FormData\`, \`crypto\` (including \`crypto.subtle\`), \`TextEncoder\`, \`TextDecoder\`,
40524
+ \`CompressionStream\`, timers and \`console\`. Bodies arrive and leave whole rather than incrementally.
40525
+ - Persist state in the database. Module state is not durable and resets when an app unloads or a worker restarts.
40526
+ - Await database calls. Mutations run through one writer per app and reply after their Postgres commit.
40527
+ - Handlers have CPU, wall-clock and heap limits. Keep loops and reads bounded.
40528
+ - Outbound fetch is available to claimed apps in actions and read-only endpoints. Private addresses are blocked.
40276
40529
 
40277
40530
  ## Deploys without an account
40278
40531
 
@@ -40410,6 +40663,22 @@ var deployFlags = {
40410
40663
  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
40664
  };
40412
40665
  var deployOptions = (input) => ({ name: value3(input.name), publicInspect: input.publicInspect });
40666
+ var deleteTableFlag = {
40667
+ deleteTable: Flag_exports.KeyValuePair("delete-table").pipe(
40668
+ Flag_exports.withDescription("Delete a table removed from the schema, confirming its current row count: --delete-table <table>=<rows> (repeatable)"),
40669
+ Flag_exports.optional
40670
+ )
40671
+ };
40672
+ var shadowFlags = {
40673
+ shadow: Flag_exports.Boolean("shadow").pipe(
40674
+ Flag_exports.withDefault(false),
40675
+ Flag_exports.withDescription("Before installing, rerun the app's live queries on the new code and report which results change or start throwing")
40676
+ ),
40677
+ failOnQueryErrors: Flag_exports.Boolean("fail-on-query-errors").pipe(
40678
+ Flag_exports.withDefault(false),
40679
+ Flag_exports.withDescription("Like --shadow, but refuse the deploy if a live query that returns a value now would throw")
40680
+ )
40681
+ };
40413
40682
  var browserFlag = {
40414
40683
  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
40684
  };
@@ -40431,8 +40700,19 @@ var initCommand = Command_exports.make("init", { template: templateFlag }, ({ te
40431
40700
  var typecheckCommand = Command_exports.make("typecheck", {}, () => Effect_exports.flatMap(config, (c2) => typecheck(c2.root))).pipe(
40432
40701
  Command_exports.withDescription("Typecheck server/, client/ and shared/ (no tsconfig.json or node_modules needed)")
40433
40702
  );
40434
- var deployCommand = Command_exports.make("deploy", deployFlags, (input) => Effect_exports.flatMap(config, (c2) => deploy(c2, deployOptions(input)))).pipe(
40435
- Command_exports.withDescription("Build and deploy. Without a login this creates a temporary unclaimed app.")
40703
+ var deployCommand = Command_exports.make(
40704
+ "deploy",
40705
+ { ...deployFlags, ...deleteTableFlag, ...shadowFlags },
40706
+ (input) => Effect_exports.flatMap(
40707
+ config,
40708
+ (c2) => deploy(c2, { ...deployOptions(input), deleteTables: value3(input.deleteTable), shadow: input.shadow, failOnQueryErrors: input.failOnQueryErrors })
40709
+ )
40710
+ ).pipe(
40711
+ Command_exports.withDescription("Build and deploy. Without a login this creates a temporary unclaimed app."),
40712
+ Command_exports.withExamples([
40713
+ { command: "eraspace deploy --delete-table drafts=12", description: "Deploy a schema without drafts, deleting its 12 rows" },
40714
+ { command: "eraspace deploy --fail-on-query-errors", description: "Deploy only if no live query starts throwing on the new code" }
40715
+ ])
40436
40716
  );
40437
40717
  var devCommand = Command_exports.make("dev", deployFlags, (input) => Effect_exports.flatMap(config, (c2) => dev(c2, deployOptions(input)))).pipe(
40438
40718
  Command_exports.withDescription("Deploy, then redeploy whenever server/, client/, shared/ or .env.eraspace.server change")
@@ -40440,14 +40720,38 @@ var devCommand = Command_exports.make("dev", deployFlags, (input) => Effect_expo
40440
40720
  var callCommand = Command_exports.make(
40441
40721
  "call",
40442
40722
  {
40443
- fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query or mutation name")),
40723
+ fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query, mutation or action name")),
40444
40724
  args: Argument_exports.String("json").pipe(Argument_exports.withDescription("Arguments, each one a JSON value"), Argument_exports.variadic())
40445
40725
  },
40446
- ({ fn: fn3, args: args2 }) => Effect_exports.flatMap(config, (c2) => call(c2, fn3, args2))
40726
+ ({ fn: fn3, args: args2 }) => Effect_exports.flatMap(config, (c2) => call2(c2, fn3, args2))
40447
40727
  ).pipe(
40448
40728
  Command_exports.withDescription("Call a handler with positional JSON arguments"),
40449
40729
  Command_exports.withExamples([{ command: `eraspace call messages '"general"'`, description: "Strings are JSON too, so quote them" }])
40450
40730
  );
40731
+ var profileCommand = Command_exports.make(
40732
+ "profile",
40733
+ {
40734
+ app: optionalArg("app", "App name or id (default: this checkout's app)"),
40735
+ seconds: Flag_exports.Int("seconds").pipe(Flag_exports.filter((n) => n >= 1 && n <= 60, (n) => `--seconds must be between 1 and 60, got ${n}`), Flag_exports.withDefault(10), Flag_exports.withDescription("How long to sample")),
40736
+ out: optionalString("out", "Where to write the .cpuprofile (default: <app>-<time>.cpuprofile)")
40737
+ },
40738
+ ({ app, seconds: seconds2, out }) => Effect_exports.flatMap(config, (c2) => profile(c2, value3(app), seconds2, value3(out)))
40739
+ ).pipe(
40740
+ Command_exports.withDescription("Sample the app's server CPU time and write a .cpuprofile that opens in Chrome DevTools against the TypeScript sources"),
40741
+ Command_exports.withExamples([{ command: "eraspace profile my-app --seconds 10", description: "Profile while you exercise the app" }])
40742
+ );
40743
+ var explainCommand = Command_exports.make(
40744
+ "explain",
40745
+ {
40746
+ fn: Argument_exports.String("fn").pipe(Argument_exports.withDescription("Query name")),
40747
+ args: Argument_exports.String("json").pipe(Argument_exports.withDescription("Arguments, each one a JSON value"), Argument_exports.variadic()),
40748
+ json: Flag_exports.Boolean("json").pipe(Flag_exports.withDefault(false), Flag_exports.withDescription("Print the full report as JSON"))
40749
+ },
40750
+ ({ fn: fn3, args: args2, json: json2 }) => Effect_exports.flatMap(config, (c2) => explain(c2, fn3, args2, json2))
40751
+ ).pipe(
40752
+ Command_exports.withDescription("Run a query once and suggest indexes for rows it filters in JavaScript"),
40753
+ Command_exports.withExamples([{ command: `eraspace explain todosByStatus '"done"'`, description: "Strings are JSON too, so quote them" }])
40754
+ );
40451
40755
  var logsCommand = Command_exports.make(
40452
40756
  "logs",
40453
40757
  {
@@ -40492,15 +40796,40 @@ var inspectionCommand = Command_exports.make(
40492
40796
  },
40493
40797
  ({ visibility, app }) => Effect_exports.flatMap(config, (c2) => inspection(c2, visibility, value3(app)))
40494
40798
  ).pipe(Command_exports.withDescription("Open or close inspection and logs to everyone"));
40495
- var yesFlag = Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm"));
40496
- var lifecycleCommand = (action, description) => Command_exports.make(
40497
- action,
40799
+ var lifecycleApp = optionalArg("app", "App name or id (default: this checkout's app)");
40800
+ var lifecycleCommand = (action, description) => Command_exports.make(action, { app: lifecycleApp }, ({ app }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, action, value3(app), false))).pipe(
40801
+ Command_exports.withDescription(description)
40802
+ );
40803
+ var changesCommand = Command_exports.make(
40804
+ "changes",
40498
40805
  {
40499
- app: optionalArg("app", "App name or id (default: this checkout's app)"),
40500
- yes: yesFlag
40806
+ since: Flag_exports.Int("since").pipe(Flag_exports.filter((n) => n >= 0, (n) => `--since must be a commit number, got ${n}`), Flag_exports.withDefault(0), Flag_exports.withDescription("Print changes committed after this commit")),
40807
+ limit: Flag_exports.Int("limit").pipe(Flag_exports.filter((n) => n >= 1 && n <= 1e4, (n) => `--limit must be between 1 and 10000, got ${n}`), Flag_exports.withDefault(1e3), Flag_exports.withDescription("Rows to print at most (whole commits)"))
40808
+ },
40809
+ ({ since, limit }) => Effect_exports.flatMap(config, (c2) => changes2(c2, since, limit))
40810
+ ).pipe(
40811
+ Command_exports.withDescription("Print recent committed row changes as NDJSON (the change log keeps 24 hours by default)"),
40812
+ Command_exports.withExamples([{ command: "eraspace changes --since 120 | jq .", description: "Changes committed after commit 120" }])
40813
+ );
40814
+ var undoCommand = Command_exports.make(
40815
+ "undo",
40816
+ {
40817
+ to: Argument_exports.Int("commit").pipe(Argument_exports.withDescription("Commit number to restore rows to")),
40818
+ yes: Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm the undo"))
40819
+ },
40820
+ ({ to, yes }) => Effect_exports.flatMap(config, (c2) => undo(c2, to, yes))
40821
+ ).pipe(
40822
+ Command_exports.withDescription("Put every row changed after a commit back as it was; needs --yes. The undo is a commit too, so it can be undone"),
40823
+ Command_exports.withExamples([{ command: "eraspace undo 120 --yes", description: "Revert everything committed after commit 120" }])
40824
+ );
40825
+ var terminateCommand = Command_exports.make(
40826
+ "terminate",
40827
+ {
40828
+ app: lifecycleApp,
40829
+ yes: Flag_exports.Boolean("yes").pipe(Flag_exports.withAlias("y"), Flag_exports.withDefault(false), Flag_exports.withDescription("Confirm that the app stops permanently"))
40501
40830
  },
40502
- ({ app, yes }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, action, value3(app), yes))
40503
- ).pipe(Command_exports.withDescription(description));
40831
+ ({ app, yes }) => Effect_exports.flatMap(config, (c2) => lifecycle(c2, "terminate", value3(app), yes))
40832
+ ).pipe(Command_exports.withDescription("Stop permanently (data kept, no more deploys); needs --yes"));
40504
40833
  var tokenCommand = Command_exports.make("token").pipe(
40505
40834
  Command_exports.withDescription("Create, list and revoke API tokens"),
40506
40835
  Command_exports.withSubcommands([
@@ -40534,6 +40863,8 @@ var command = eraspace.pipe(
40534
40863
  deployCommand,
40535
40864
  devCommand,
40536
40865
  callCommand,
40866
+ explainCommand,
40867
+ profileCommand,
40537
40868
  logsCommand,
40538
40869
  inspectCommand,
40539
40870
  authCommand,
@@ -40543,7 +40874,9 @@ var command = eraspace.pipe(
40543
40874
  inspectionCommand,
40544
40875
  lifecycleCommand("archive", "Stop serving and keep the data"),
40545
40876
  lifecycleCommand("restore", "Serve an archived app again"),
40546
- lifecycleCommand("terminate", "Stop permanently (data kept, no more deploys); needs --yes"),
40877
+ terminateCommand,
40878
+ changesCommand,
40879
+ undoCommand,
40547
40880
  tokenCommand,
40548
40881
  ...hidden
40549
40882
  ])