@weborigami/origami 0.6.17 → 0.7.0-beta.2

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.
@@ -1,10 +1,10 @@
1
1
  import {
2
- TraverseError,
3
- Tree,
4
2
  keysFromPath,
5
3
  trailingSlash,
4
+ TraverseError,
5
+ Tree,
6
6
  } from "@weborigami/async-tree";
7
- import { formatError } from "@weborigami/language";
7
+ import { formatError, systemCache, SystemCacheMap } from "@weborigami/language";
8
8
  import { ServerResponse } from "node:http";
9
9
  import constructResponse from "./constructResponse.js";
10
10
  import parsePostData from "./parsePostData.js";
@@ -13,35 +13,30 @@ import parsePostData from "./parsePostData.js";
13
13
  * Copy a constructed response to a ServerResponse. Return true if the response
14
14
  * was successfully copied, and false if there was a problem.
15
15
  *
16
- * @param {Response} constructed
16
+ * @param {Response} original
17
17
  * @param {ServerResponse} response
18
18
  */
19
- async function copyResponse(constructed, response) {
20
- response.statusCode = constructed.status;
21
- response.statusMessage = constructed.statusText;
19
+ async function copyResponse(original, response) {
20
+ const clone = original.clone();
21
+ response.statusCode = clone.status;
22
+ response.statusMessage = clone.statusText;
22
23
 
23
24
  // @ts-ignore Headers has an iterator in ES2022 but tsc doesn't know that.
24
- for (const [key, value] of constructed.headers) {
25
+ for (const [key, value] of clone.headers) {
25
26
  response.setHeader(key, value);
26
27
  }
27
28
 
28
- if (constructed.body) {
29
- try {
30
- // Write the response body to the ServerResponse.
31
- const reader = constructed.body.getReader();
32
- let { done, value } = await reader.read();
33
- while (!done) {
34
- response.write(value);
35
- ({ done, value } = await reader.read());
36
- }
37
- response.end();
38
- } catch (/** @type {any} */ error) {
39
- console.error(error.message);
40
- return false;
29
+ if (clone.body) {
30
+ // Write the response body
31
+ const reader = clone.body.getReader();
32
+ let { done, value } = await reader.read();
33
+ while (!done) {
34
+ response.write(value);
35
+ ({ done, value } = await reader.read());
41
36
  }
42
37
  }
43
38
 
44
- return true;
39
+ response.end();
45
40
  }
46
41
 
47
42
  /**
@@ -54,36 +49,68 @@ async function copyResponse(constructed, response) {
54
49
  export async function handleRequest(request, response, map) {
55
50
  // For parsing purposes, we assume HTTPS -- it doesn't affect parsing.
56
51
  const url = new URL(request.url ?? "", `https://${request.headers.host}`);
57
- const keys = keysFromUrl(url);
58
52
 
53
+ // Do we already have an ETag for this resource?
54
+ let cachePath = SystemCacheMap.joinPath("_site", url.pathname.slice(1));
55
+ if (url.pathname.endsWith("/")) {
56
+ cachePath += "index.html";
57
+ }
58
+ const cacheEntry = systemCache.get(cachePath)?.value;
59
+ const etag = cacheEntry?.headers?.get("Etag");
60
+ if (etag) {
61
+ // Does the client already have this version?
62
+ const ifNoneMatch = request?.headers?.["if-none-match"];
63
+ if (ifNoneMatch === etag) {
64
+ // Client already has this version
65
+ response.writeHead(304, {
66
+ "Cache-Control": "no-cache",
67
+ ETag: etag,
68
+ });
69
+ response.end();
70
+ return true;
71
+ }
72
+ }
73
+
74
+ const keys = keysFromUrl(url);
59
75
  const data = request.method === "POST" ? await parsePostData(request) : null;
60
76
 
61
77
  // Ask the tree for the resource with those keys.
62
- let resource;
63
78
  try {
64
- resource = await Tree.traverseOrThrow(map, ...keys);
65
-
66
- // If resource is a function, invoke to get the object we want to return.
67
- // For a POST request, pass the data to the function.
68
- if (typeof resource === "function") {
69
- resource = data ? await resource(data) : await resource();
79
+ // We wrap the tree traversal in a call that will both set the etag for this
80
+ // resource and copy the constructed response to the ServerResponse. The
81
+ // etag is already included in the response headers so we don't need to
82
+ // receive it here.
83
+ const constructed = await systemCache.getOrInsertComputedAsync(
84
+ cachePath,
85
+ async () => {
86
+ let resource = await Tree.traverseOrThrow(map, ...keys);
87
+
88
+ // If resource is a function, invoke to get the object we want to return.
89
+ // For a POST request, pass the data to the function.
90
+ if (typeof resource === "function") {
91
+ resource = data ? await resource(data) : await resource();
92
+ }
93
+
94
+ // Construct the response
95
+ return resource != null
96
+ ? await constructResponse(request, resource)
97
+ : null;
98
+ },
99
+ );
100
+
101
+ // Copy the constructed (and cached) response to the server response
102
+ if (constructed) {
103
+ await copyResponse(constructed, response);
104
+ return true;
70
105
  }
71
-
72
- if (resource == null) {
73
- return false;
74
- }
75
-
76
- // Construct the response.
77
- const constructed = await constructResponse(request, resource);
78
-
79
- // Copy the construct response to the ServerResponse and return true if
80
- // the response was valid.
81
- return copyResponse(constructed, response);
82
106
  } catch (/** @type {any} */ error) {
83
107
  // Display an error
84
- respondWithError(response, error);
108
+ await respondWithError(response, error);
85
109
  return true;
86
110
  }
111
+
112
+ // No resource found at this path.
113
+ return false;
87
114
  }
88
115
 
89
116
  export function keysFromUrl(url) {
@@ -1,38 +0,0 @@
1
- import { args, isStringlike, toString, Tree } from "@weborigami/async-tree";
2
-
3
- /**
4
- * Given an old tree and a new tree, return a tree of changes indicated
5
- * by the values: "added", "changed", or "deleted".
6
- *
7
- * @typedef {import("@weborigami/async-tree").Maplike} Maplike
8
- *
9
- * @param {Maplike} oldMaplike
10
- * @param {Maplike} newMaplike
11
- */
12
- export default async function changes(oldMaplike, newMaplike) {
13
- const oldTree = await args.map(oldMaplike, "Dev.changes", {
14
- deep: true,
15
- position: 1,
16
- });
17
- const newTree = await args.map(newMaplike, "Dev.changes", {
18
- deep: true,
19
- position: 2,
20
- });
21
-
22
- const combination = await Tree.combine(oldTree, newTree, compare);
23
- return combination;
24
- }
25
-
26
- function compare(oldValue, newValue) {
27
- if (oldValue !== undefined && newValue === undefined) {
28
- return "deleted";
29
- } else if (oldValue === undefined && newValue !== undefined) {
30
- return "added";
31
- } else if (isStringlike(oldValue) && isStringlike(newValue)) {
32
- const oldText = toString(oldValue);
33
- const newText = toString(newValue);
34
- return oldText === newText ? undefined : "changed";
35
- } else {
36
- return oldValue === newValue ? undefined : "changed";
37
- }
38
- }
@@ -1,6 +0,0 @@
1
- import { projectRoot } from "@weborigami/language";
2
-
3
- export default async function project() {
4
- console.warn("Origami.project has been renamed to Origami.projectRoot");
5
- return projectRoot();
6
- }