apps 0.0.1-beta.79 → 0.0.1-beta.80

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/js/ui/serving.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  } from "../chunk-KLYSEHUD.js";
4
4
  import "../chunk-JPEUKEV4.js";
5
5
  import {
6
+ UiFailed,
6
7
  UiForbidden
7
8
  } from "../chunk-CDXQDN7W.js";
8
9
  import {
@@ -17,7 +18,9 @@ import {
17
18
  HttpServerResponse_exports
18
19
  } from "../chunk-EOPQ727L.js";
19
20
  import {
20
- Effect_exports
21
+ Effect_exports,
22
+ Option_exports,
23
+ Stream_exports
21
24
  } from "../chunk-IFJU5SYQ.js";
22
25
  import "../chunk-CZ7CSFO4.js";
23
26
 
@@ -8526,12 +8529,14 @@ var appDocument = (options) => Effect_exports.gen(function* () {
8526
8529
  const pathname = yield* Effect_exports.try(() => new URL(request.url, options.origin).pathname).pipe(
8527
8530
  Effect_exports.mapError(() => new UiForbidden())
8528
8531
  );
8529
- const file = pathname === "/" ? void 0 : yield* options.asset(pathname.slice(1));
8530
- if (file !== void 0 && file.contentType !== "text/html")
8532
+ const path = Option_exports.getOrUndefined(assetPath(pathname.slice(1)));
8533
+ const file = pathname === "/" || path === void 0 || path === "index.html" ? void 0 : yield* options.asset(path);
8534
+ if (file !== void 0 && "body" in file)
8531
8535
  return HttpServerResponse_exports.uint8Array(file.body, {
8532
8536
  contentType: file.contentType,
8533
8537
  headers: appPrivateHeaders
8534
8538
  });
8539
+ if (file !== void 0) return yield* storedFile(file, "private, no-cache");
8535
8540
  if (pathname.includes(".") && pathname !== "/index.html")
8536
8541
  return HttpServerResponse_exports.empty({ status: 404 });
8537
8542
  const notice = options.accounts;
@@ -8542,7 +8547,7 @@ var appDocument = (options) => Effect_exports.gen(function* () {
8542
8547
  headers: appPrivateHeaders
8543
8548
  });
8544
8549
  const document = yield* options.asset("index.html");
8545
- if (document === void 0)
8550
+ if (document === void 0 || !("body" in document))
8546
8551
  return HttpServerResponse_exports.text("This app has no UI.", {
8547
8552
  status: 404,
8548
8553
  headers: appPrivateHeaders
@@ -8564,21 +8569,92 @@ var appDocument = (options) => Effect_exports.gen(function* () {
8564
8569
  { contentType: "text/html", headers: appPrivateHeaders }
8565
8570
  );
8566
8571
  });
8572
+ var storedAppFile = (stored, read) => {
8573
+ const unavailable = () => new UiFailed({ reason: "unavailable" });
8574
+ return {
8575
+ contentType: stored.contentType,
8576
+ size: stored.size,
8577
+ etag: stored.etag,
8578
+ read: (range) => read(range).pipe(
8579
+ Effect_exports.mapError(unavailable),
8580
+ Effect_exports.flatMap(
8581
+ Option_exports.match({
8582
+ onNone: () => Effect_exports.fail(unavailable()),
8583
+ onSome: (found) => Effect_exports.succeed(found.body.pipe(Stream_exports.mapError(unavailable)))
8584
+ })
8585
+ )
8586
+ )
8587
+ };
8588
+ };
8589
+ var assetPath = (path) => Option_exports.liftThrowable(() => path.split("/").map(decodeURIComponent).join("/"))();
8590
+ var etagMatches = (header, etag) => header?.split(",").some(
8591
+ (candidate) => candidate.trim() === "*" || candidate.trim().replace(/^W\//, "") === etag.replace(/^W\//, "")
8592
+ ) === true;
8593
+ var requestedRange = (header, ifRange, etag, size) => {
8594
+ if (header === void 0 || ifRange !== void 0 && ifRange.trim() !== etag) return void 0;
8595
+ const match = /^bytes=(\d*)-(\d*)$/u.exec(header.trim());
8596
+ if (match === null) return void 0;
8597
+ const [, first = "", last = ""] = match;
8598
+ if (first === "" && last === "") return void 0;
8599
+ if (first === "") {
8600
+ const suffix = Number(last);
8601
+ if (suffix === 0) return "unsatisfiable";
8602
+ return { start: Math.max(0, size - suffix), end: size - 1 };
8603
+ }
8604
+ const start = Number(first);
8605
+ const end = last === "" ? size - 1 : Math.min(Number(last), size - 1);
8606
+ if (start >= size) return "unsatisfiable";
8607
+ if (end < start) return void 0;
8608
+ return { start, end };
8609
+ };
8610
+ var storedFile = (file, cacheControl) => Effect_exports.gen(function* () {
8611
+ const request = yield* HttpServerRequest_exports.HttpServerRequest;
8612
+ const etag = `"${file.etag}"`;
8613
+ const headers = {
8614
+ ...appPrivateHeaders,
8615
+ "cache-control": cacheControl,
8616
+ vary: "Cookie",
8617
+ etag,
8618
+ "accept-ranges": "bytes"
8619
+ };
8620
+ if (etagMatches(request.headers["if-none-match"], etag))
8621
+ return HttpServerResponse_exports.empty({ status: 304, headers });
8622
+ const range = requestedRange(
8623
+ request.headers.range,
8624
+ request.headers["if-range"],
8625
+ etag,
8626
+ file.size
8627
+ );
8628
+ if (range === "unsatisfiable")
8629
+ return HttpServerResponse_exports.empty({
8630
+ status: 416,
8631
+ headers: { ...headers, "content-range": `bytes */${file.size}` }
8632
+ });
8633
+ const length = range === void 0 ? file.size : range.end - range.start + 1;
8634
+ const body = yield* file.read(
8635
+ range === void 0 ? void 0 : { offset: range.start, length }
8636
+ );
8637
+ return HttpServerResponse_exports.stream(body, {
8638
+ status: range === void 0 ? 200 : 206,
8639
+ contentType: file.contentType,
8640
+ contentLength: length,
8641
+ headers: range === void 0 ? headers : { ...headers, "content-range": `bytes ${range.start}-${range.end}/${file.size}` }
8642
+ });
8643
+ });
8567
8644
  var appAsset = (asset, build, path) => Effect_exports.gen(function* () {
8568
- if (asset === void 0 || asset.contentType === "text/html")
8645
+ if (asset === void 0 || path === "index.html")
8569
8646
  return HttpServerResponse_exports.empty({ status: 404, headers: appPrivateHeaders });
8647
+ const immutable = "private, max-age=31536000, immutable";
8648
+ if (!("body" in asset)) return yield* storedFile(asset, immutable);
8570
8649
  const request = yield* HttpServerRequest_exports.HttpServerRequest;
8571
8650
  const etag = `W/"${encodeURIComponent(build)}/${encodeURIComponent(path)}"`;
8572
8651
  const headers = {
8573
8652
  ...appPrivateHeaders,
8574
- "cache-control": "private, max-age=31536000, immutable",
8653
+ "cache-control": immutable,
8575
8654
  vary: "Cookie",
8576
8655
  etag
8577
8656
  };
8578
- const matches = request.headers["if-none-match"]?.split(",").some(
8579
- (candidate) => candidate.trim() === "*" || candidate.trim().replace(/^W\//, "") === etag.slice(2)
8580
- );
8581
- return matches ? HttpServerResponse_exports.empty({ status: 304, headers }) : HttpServerResponse_exports.uint8Array(asset.body, { contentType: asset.contentType, headers });
8657
+ return etagMatches(request.headers["if-none-match"], etag) ? HttpServerResponse_exports.empty({ status: 304, headers }) : HttpServerResponse_exports.uint8Array(asset.body, { contentType: asset.contentType, headers });
8582
8658
  });
8583
8659
 
8584
8660
  // src/implementation/ui-watch.ts
@@ -8586,5 +8662,7 @@ var appWatchScript = appWatchSource;
8586
8662
  export {
8587
8663
  appAsset,
8588
8664
  appDocument,
8589
- appWatchScript
8665
+ appWatchScript,
8666
+ assetPath,
8667
+ storedAppFile
8590
8668
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apps",
3
- "version": "0.0.1-beta.79",
3
+ "version": "0.0.1-beta.80",
4
4
  "description": "TypeScript apps with typed tools, accounts, storage, and UI.",
5
5
  "type": "module",
6
6
  "sideEffects": false,