@occultist/occultist 0.0.4 → 0.0.6

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.
Files changed (82) hide show
  1. package/dist/accept.js +0 -1
  2. package/dist/actions/actionSets.d.ts +3 -3
  3. package/dist/actions/actions.d.ts +69 -48
  4. package/dist/actions/actions.js +39 -4
  5. package/dist/actions/context.d.ts +15 -11
  6. package/dist/actions/context.js +5 -0
  7. package/dist/actions/meta.d.ts +18 -12
  8. package/dist/actions/meta.js +114 -38
  9. package/dist/actions/spec.d.ts +3 -3
  10. package/dist/actions/types.d.ts +45 -16
  11. package/dist/actions/writer.d.ts +1 -1
  12. package/dist/actions/writer.test.js +2 -2
  13. package/dist/cache/cache.d.ts +3 -3
  14. package/dist/cache/cache.js +111 -42
  15. package/dist/cache/etag.test.js +1 -1
  16. package/dist/cache/file.d.ts +33 -1
  17. package/dist/cache/file.js +92 -10
  18. package/dist/cache/memory.d.ts +12 -2
  19. package/dist/cache/memory.js +63 -1
  20. package/dist/cache/types.d.ts +51 -22
  21. package/dist/errors.d.ts +1 -1
  22. package/dist/jsonld.d.ts +1 -1
  23. package/dist/makeTypeDefs.d.ts +2 -2
  24. package/dist/mod.d.ts +17 -15
  25. package/dist/mod.js +17 -15
  26. package/dist/processAction.d.ts +2 -2
  27. package/dist/processAction.js +1 -1
  28. package/dist/registry.d.ts +74 -8
  29. package/dist/registry.js +70 -8
  30. package/dist/registry.test.js +1 -1
  31. package/dist/scopes.d.ts +8 -8
  32. package/dist/scopes.js +8 -5
  33. package/dist/utils/contextBuilder.d.ts +1 -1
  34. package/dist/utils/getActionContext.d.ts +2 -2
  35. package/dist/utils/getPropertyValueSpecifications.d.ts +1 -1
  36. package/dist/utils/getRequestBodyValues.d.ts +3 -3
  37. package/dist/utils/getRequestIRIValues.d.ts +2 -2
  38. package/dist/utils/isPopulatedObject.js +1 -1
  39. package/dist/utils/makeAppendProblemDetails.d.ts +1 -1
  40. package/dist/utils/makeURLPattern.js +1 -0
  41. package/dist/utils/parseSearchParams.d.ts +2 -2
  42. package/dist/validators.d.ts +2 -2
  43. package/dist/validators.js +2 -2
  44. package/lib/accept.test.ts +1 -1
  45. package/lib/accept.ts +0 -2
  46. package/lib/actions/actionSets.ts +4 -4
  47. package/lib/actions/actions.ts +159 -99
  48. package/lib/actions/context.ts +22 -10
  49. package/lib/actions/meta.ts +140 -55
  50. package/lib/actions/path.test.ts +1 -1
  51. package/lib/actions/path.ts +1 -1
  52. package/lib/actions/spec.ts +3 -3
  53. package/lib/actions/types.ts +60 -15
  54. package/lib/actions/writer.test.ts +2 -2
  55. package/lib/actions/writer.ts +1 -1
  56. package/lib/cache/cache.ts +138 -52
  57. package/lib/cache/etag.test.ts +1 -1
  58. package/lib/cache/file.ts +113 -12
  59. package/lib/cache/memory.ts +85 -3
  60. package/lib/cache/types.ts +70 -23
  61. package/lib/errors.ts +1 -1
  62. package/lib/jsonld.ts +1 -1
  63. package/lib/makeTypeDefs.ts +5 -5
  64. package/lib/mod.ts +17 -15
  65. package/lib/processAction.ts +14 -14
  66. package/lib/registry.test.ts +1 -1
  67. package/lib/registry.ts +96 -19
  68. package/lib/request.ts +1 -1
  69. package/lib/scopes.test.ts +2 -2
  70. package/lib/scopes.ts +14 -11
  71. package/lib/utils/contextBuilder.ts +3 -3
  72. package/lib/utils/getActionContext.ts +4 -4
  73. package/lib/utils/getInternalName.ts +1 -1
  74. package/lib/utils/getPropertyValueSpecifications.ts +4 -4
  75. package/lib/utils/getRequestBodyValues.ts +5 -5
  76. package/lib/utils/getRequestIRIValues.ts +4 -4
  77. package/lib/utils/isPopulatedObject.ts +1 -1
  78. package/lib/utils/makeAppendProblemDetails.ts +1 -1
  79. package/lib/utils/makeURLPattern.ts +1 -0
  80. package/lib/utils/parseSearchParams.ts +2 -2
  81. package/lib/validators.ts +5 -5
  82. package/package.json +4 -2
@@ -1,8 +1,13 @@
1
- import { CacheMiddleware } from '../cache/cache.js';
2
- import { processAction } from '../processAction.js';
3
- import { joinPaths } from '../utils/joinPaths.js';
4
- import { CacheContext, Context } from './context.js';
1
+ import { Accept } from "../accept.js";
2
+ import { CacheMiddleware } from "../cache/cache.js";
3
+ import { ProblemDetailsError } from "../errors.js";
4
+ import { processAction } from "../processAction.js";
5
+ import { WrappedRequest } from "../request.js";
6
+ import { joinPaths } from "../utils/joinPaths.js";
7
+ import { ActionSet } from "./actionSets.js";
8
+ import { CacheContext, Context } from "./context.js";
5
9
  import { Path } from "./path.js";
10
+ import { ResponseWriter } from "./writer.js";
6
11
  export const BeforeDefinition = 0;
7
12
  export const AfterDefinition = 1;
8
13
  const cacheMiddleware = new CacheMiddleware();
@@ -23,6 +28,7 @@ export class ActionMeta {
23
28
  acceptCache = new Set();
24
29
  compressBeforeCache = false;
25
30
  cacheOccurance = BeforeDefinition;
31
+ auth;
26
32
  cache = [];
27
33
  serverTiming = false;
28
34
  constructor(rootIRI, method, name, uriTemplate, registry, writer, scope) {
@@ -41,19 +47,46 @@ export class ActionMeta {
41
47
  finalize() {
42
48
  this.#setAcceptCache();
43
49
  }
44
- async handleRequest({ startTime, contentType, language: _language, encoding: _encoding, url, req, writer, spec, handler, }) {
50
+ async perform(req) {
51
+ const actionSet = new ActionSet(this.rootIRI, this.method, this.path.normalized, [this]);
52
+ const wrapped = new WrappedRequest(this.rootIRI, req);
53
+ const writer = new ResponseWriter();
54
+ const accept = Accept.from(req);
55
+ const url = new URL(wrapped.url);
56
+ const result = actionSet.matches(wrapped.method, url.pathname, accept);
57
+ if (result.type === 'match') {
58
+ const handler = this.action.handlerFor(result.contentType);
59
+ return this.handleRequest({
60
+ startTime: performance.now(),
61
+ contentType: result.contentType,
62
+ url: url.toString(),
63
+ req: wrapped,
64
+ writer,
65
+ spec: this.action.spec,
66
+ handler,
67
+ });
68
+ }
69
+ return new Response(null, { status: 404 });
70
+ }
71
+ /**
72
+ *
73
+ */
74
+ async handleRequest({ contentType, url, req, writer, spec, handler, cacheHitHeader, startTime, }) {
45
75
  const state = {};
46
76
  const headers = new Headers();
77
+ let authKey;
78
+ let auth = {};
47
79
  let ctx;
48
80
  let cacheCtx;
49
81
  let prevTime = startTime;
82
+ let performServerTiming = this.serverTiming && startTime != null;
50
83
  const serverTiming = (name) => {
51
84
  const nextTime = performance.now();
52
85
  const duration = nextTime - prevTime;
53
- headers.append('Server-Timing', `${name};dur=${duration.toPrecision(2)}`);
86
+ headers.append('Server-Timing', `${name};dur=${duration.toFixed(2)}`);
54
87
  prevTime = nextTime;
55
88
  };
56
- if (this.serverTiming)
89
+ if (performServerTiming)
57
90
  serverTiming('enter');
58
91
  // add auth check
59
92
  if (this.hints.length !== 0) {
@@ -67,58 +100,70 @@ export class ActionMeta {
67
100
  ctx.status = 200;
68
101
  ctx.body = handler.handler;
69
102
  }
70
- if (this.serverTiming)
103
+ if (performServerTiming)
71
104
  serverTiming('handle');
72
105
  };
73
106
  {
74
107
  const upstream = next;
75
108
  next = async () => {
76
- const res = await processAction({
77
- iri: url,
78
- req,
79
- spec: spec ?? {},
80
- state,
81
- action: this.action,
82
- });
109
+ let processed;
110
+ if (spec != null) {
111
+ processed = await processAction({
112
+ iri: url,
113
+ req,
114
+ spec: spec ?? {},
115
+ state,
116
+ action: this.action,
117
+ });
118
+ }
83
119
  ctx = new Context({
84
120
  req,
85
121
  url,
86
122
  contentType,
87
- public: this.public,
123
+ public: this.public && authKey == null,
124
+ auth,
125
+ authKey,
88
126
  handler,
89
- params: res.params,
90
- query: res.query,
91
- payload: res.payload,
127
+ params: processed.params ?? {},
128
+ query: processed.query ?? {},
129
+ payload: processed.payload ?? {},
92
130
  });
93
131
  if (contentType != null) {
94
132
  ctx.headers.set('Content-Type', contentType);
95
133
  }
96
- if (this.serverTiming)
134
+ if (performServerTiming)
97
135
  serverTiming('payload');
98
136
  await upstream();
99
137
  };
100
138
  }
101
139
  if (this.cache.length > 0) {
102
- cacheCtx = new CacheContext({
103
- req,
104
- url,
105
- contentType,
106
- public: this.public,
107
- handler,
108
- params: {},
109
- query: {},
110
- });
111
- const descriptors = this.cache.map(args => {
112
- return {
113
- contentType,
114
- action: this.action,
115
- request: req,
116
- args,
117
- };
118
- });
119
140
  const upstream = next;
120
141
  next = async () => {
142
+ cacheCtx = new CacheContext({
143
+ req,
144
+ url,
145
+ contentType,
146
+ public: this.public && authKey == null,
147
+ auth,
148
+ authKey,
149
+ handler,
150
+ params: {},
151
+ query: {},
152
+ });
153
+ const descriptors = this.cache.map(args => {
154
+ return {
155
+ contentType,
156
+ semantics: args.semantics ?? req.method.toLowerCase(),
157
+ action: this.action,
158
+ request: req,
159
+ args,
160
+ };
161
+ });
121
162
  await cacheMiddleware.use(descriptors, cacheCtx, async () => {
163
+ // write any cache headers to the response headers.
164
+ // this should be reviewed as it may be unsafe to
165
+ // allow a handler to override these headers.
166
+ writer.mergeHeaders(cacheCtx.headers);
122
167
  // cache was not hit if in this function
123
168
  await upstream();
124
169
  // the cache middleware requires these values are set
@@ -137,11 +182,42 @@ export class ActionMeta {
137
182
  });
138
183
  };
139
184
  }
185
+ if (this.auth != null) {
186
+ const upstream = next;
187
+ next = async () => {
188
+ const res = await this.auth(req);
189
+ if (Array.isArray(res) &&
190
+ typeof res[0] === 'string' &&
191
+ res.length > 0) {
192
+ authKey = res[0];
193
+ auth = res[1];
194
+ await upstream();
195
+ }
196
+ else if (this.public) {
197
+ await upstream();
198
+ }
199
+ else {
200
+ // Failed authentication on a private endpoint.
201
+ throw new ProblemDetailsError(404, {
202
+ title: 'Not found',
203
+ });
204
+ }
205
+ };
206
+ }
140
207
  try {
141
208
  await next();
142
209
  if (cacheCtx?.hit) {
143
- if (this.serverTiming)
210
+ if (performServerTiming)
144
211
  serverTiming('hit');
212
+ if (Array.isArray(cacheHitHeader)) {
213
+ cacheCtx.headers.set(cacheHitHeader[0], cacheHitHeader[1]);
214
+ }
215
+ else if (typeof cacheHitHeader === 'string') {
216
+ cacheCtx.headers.set(cacheHitHeader, 'HIT');
217
+ }
218
+ else if (cacheHitHeader) {
219
+ cacheCtx.headers.set('X-Cache', 'HIT');
220
+ }
145
221
  // set the ctx so the writer has access to the cached values.
146
222
  ctx = cacheCtx;
147
223
  }
@@ -1,6 +1,6 @@
1
- import type { JSONPrimitive, JSONValue, OrArray, RecursiveDigit, RecursiveIncrement, TypeDef } from "../jsonld.js";
2
- import { Action } from "./actions.js";
3
- import type { Context } from './context.js';
1
+ import type { JSONPrimitive, JSONValue, OrArray, RecursiveDigit, RecursiveIncrement, TypeDef } from "../jsonld.ts";
2
+ import { Action } from "./actions.ts";
3
+ import type { Context } from './context.ts';
4
4
  export type EmptyState = Record<string, unknown>;
5
5
  export type EmptySpec = Map<PropertyKey, never>;
6
6
  export type ContextState = Record<string, unknown>;
@@ -1,11 +1,30 @@
1
- import type { HTTPWriter } from "./writer.js";
2
- import type { Registry } from '../registry.js';
3
- import type { Scope } from "../scopes.js";
4
- import type { ContextState, ActionSpec } from "./spec.js";
5
- import type { Context } from "./context.js";
1
+ import type { HTTPWriter } from "./writer.ts";
2
+ import type { Registry } from '../registry.ts';
3
+ import type { Scope } from "../scopes.ts";
4
+ import type { ContextState, ActionSpec } from "./spec.ts";
5
+ import type { Context } from "./context.ts";
6
6
  import type { ServerResponse } from "node:http";
7
- import type { JSONObject, TypeDef } from "../jsonld.js";
8
- import type { HandlerDefinition } from "../mod.js";
7
+ import type { JSONObject, TypeDef } from "../jsonld.ts";
8
+ import type { HandlerDefinition } from "../mod.ts";
9
+ export type CacheHitHeader = boolean | string | [header: string, value: string];
10
+ export type AuthState = Record<string, unknown>;
11
+ /**
12
+ * Middleware that identifies the authenticating agent from the request
13
+ * and confirms they have access to the resource.
14
+ *
15
+ * When successfully authenticated the middleware should return with
16
+ * an array of two values. The first being a key unique to the user, or
17
+ * group, which can access this resource. This key is used for varying
18
+ * private cache so should alway vary on the user if personalized information
19
+ * would be returned in the response.
20
+ *
21
+ * The second response item is optional and should be an object holding identifying
22
+ * information such as permissions or details of the user or group that might be used
23
+ * when forming the response.
24
+ *
25
+ * @param req The request.
26
+ */
27
+ export type AuthMiddleware<Auth extends AuthState = AuthState> = (req: Request) => void | Promise<void> | [authKey: string, auth?: Auth] | Promise<[authKey: string, auth?: Auth]>;
9
28
  export type HintLink = {
10
29
  href: string;
11
30
  rel?: string | string[];
@@ -43,33 +62,34 @@ export type HandlerValue = Exclude<BodyInit, ReadableStream>;
43
62
  * An action handler function that is passed a context object.
44
63
  * Responses should be set on the context object.
45
64
  */
46
- export type HandlerFn<State extends ContextState = ContextState, Spec extends ActionSpec = ActionSpec> = (ctx: Context<State, Spec>) => void | Promise<void>;
65
+ export type HandlerFn<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> = (ctx: Context<State, Auth, Spec>) => void | Promise<void>;
47
66
  /**
48
67
  * A handler object argument.
49
68
  *
50
69
  * Occultist extensions can use this handler argument method to provide arguments
51
70
  * which are usually defined while defining the action.
52
71
  */
53
- export interface HandlerObj<State extends ContextState = ContextState, Spec extends ActionSpec = ActionSpec> {
72
+ export interface HandlerObj<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> {
54
73
  contentType: string | string[];
55
- handler: HandlerFn<State, Spec> | HandlerValue;
74
+ handler: HandlerFn<State, Auth, Spec> | HandlerValue;
56
75
  meta?: HandlerMeta;
57
76
  hints?: HintArgs;
58
77
  }
59
78
  /**
60
79
  * Handler arguments for an action.
61
80
  */
62
- export type HandlerArgs<State extends ContextState = ContextState, Spec extends ActionSpec = ActionSpec> = HandlerValue | HandlerFn<State, Spec> | HandlerObj<State, Spec>;
81
+ export type HandlerArgs<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> = HandlerValue | HandlerFn<State, Auth, Spec> | HandlerObj<State, Auth, Spec>;
63
82
  export type HandleRequestArgs = {
64
- startTime: number;
65
83
  contentType?: string;
66
84
  language?: string;
67
85
  encoding?: string;
68
86
  url: string;
69
87
  req: Request;
70
88
  writer: HTTPWriter;
89
+ startTime?: number;
90
+ cacheHitHeader?: CacheHitHeader;
71
91
  };
72
- export interface ImplementedAction<State extends ContextState = ContextState, Spec extends ActionSpec = ActionSpec> {
92
+ export interface ImplementedAction<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> {
73
93
  readonly public: boolean;
74
94
  readonly method: string;
75
95
  readonly term?: string;
@@ -81,15 +101,24 @@ export interface ImplementedAction<State extends ContextState = ContextState, Sp
81
101
  readonly spec: Spec;
82
102
  readonly registry: Registry;
83
103
  readonly scope?: Scope;
84
- readonly handlers: HandlerDefinition<State, Spec>[];
104
+ readonly handlers: HandlerDefinition<State, Auth, Spec>[];
85
105
  readonly contentTypes: string[];
86
106
  readonly context: JSONObject;
87
107
  /**
88
- * @todo
89
- *
90
108
  * Creates a URL compatible with this action.
91
109
  */
92
110
  url(): string;
111
+ /**
112
+ * Retrives the handler configured for the given content type.
113
+ *
114
+ * @param contentType The content type.
115
+ */
116
+ handlerFor(contentType: string): HandlerDefinition<State, Auth, Spec> | undefined;
117
+ /**
118
+ * Performs this action using the given fetch Request
119
+ * returning a Response.
120
+ */
121
+ perform(req: Request): Promise<Response>;
93
122
  /**
94
123
  * @todo
95
124
  *
@@ -1,5 +1,5 @@
1
1
  import { ServerResponse } from 'node:http';
2
- import type { HintArgs } from './types.js';
2
+ import type { HintArgs } from './types.ts';
3
3
  export type ResponseTypes = ServerResponse | Response;
4
4
  export type ResponseBody = BodyInit;
5
5
  export interface HTTPWriter {
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import assert from 'node:assert/strict';
3
3
  import test from 'node:test';
4
- import { ResponseWriter } from './writer.js';
4
+ import { ResponseWriter } from "./writer.js";
5
5
  test('Writer writes hints', () => {
6
6
  return new Promise((resolve, reject) => {
7
7
  const server = createServer();
@@ -16,7 +16,7 @@ test('Writer writes hints', () => {
16
16
  fetchPriority: 'high',
17
17
  },
18
18
  {
19
- href: 'https://example.com/main.js',
19
+ href: 'https://example.com/main.ts',
20
20
  as: 'script',
21
21
  preload: true,
22
22
  fetchPriority: 'low',
@@ -1,5 +1,5 @@
1
- import { CacheContext, NextFn, Registry } from '../mod.js';
2
- import type { CacheBuilder, CacheEntryDescriptor, CacheETagArgs, CacheETagInstanceArgs, CacheHTTPArgs, CacheHTTPInstanceArgs, CacheMeta, CacheStorage, CacheStoreArgs, CacheStoreInstanceArgs, UpstreamCache } from './types.js';
1
+ import { CacheContext, type NextFn, Registry } from '../mod.ts';
2
+ import type { CacheBuilder, CacheEntryDescriptor, CacheETagArgs, CacheETagInstanceArgs, CacheHTTPArgs, CacheHTTPInstanceArgs, CacheMeta, CacheStorage, CacheStoreArgs, CacheStoreInstanceArgs, UpstreamCache } from './types.ts';
3
3
  export declare class Cache implements CacheBuilder {
4
4
  #private;
5
5
  constructor(registry: Registry, cacheMeta: CacheMeta, storage: CacheStorage, upstream?: UpstreamCache);
@@ -22,7 +22,7 @@ export declare class Cache implements CacheBuilder {
22
22
  */
23
23
  store(args?: CacheStoreArgs): CacheStoreInstanceArgs;
24
24
  push(_req: Request): Promise<void>;
25
- invalidate(_req: Request): Promise<void>;
25
+ invalidate(key: string, url: string): Promise<void>;
26
26
  }
27
27
  export declare class CacheMiddleware {
28
28
  #private;
@@ -1,5 +1,14 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { EtagConditions } from './etag.js';
2
+ import { EtagConditions } from "./etag.js";
3
+ const supportedSemantics = [
4
+ 'options',
5
+ 'head',
6
+ 'get',
7
+ 'post',
8
+ 'put',
9
+ 'delete',
10
+ 'query',
11
+ ];
3
12
  export class Cache {
4
13
  #registry;
5
14
  #cacheMeta;
@@ -54,7 +63,14 @@ export class Cache {
54
63
  }
55
64
  async push(_req) {
56
65
  }
57
- async invalidate(_req) {
66
+ async invalidate(key, url) {
67
+ const promises = [
68
+ this.#cacheMeta.invalidate(key),
69
+ this.#storage.invalidate(key),
70
+ ];
71
+ if (this.#upstream != null)
72
+ promises.push(this.upstream.invalidate(url));
73
+ await Promise.all(promises);
58
74
  }
59
75
  }
60
76
  export class CacheMiddleware {
@@ -78,9 +94,13 @@ export class CacheMiddleware {
78
94
  }
79
95
  return false;
80
96
  });
81
- if (descriptor == null) {
97
+ if (descriptor == null || !supportedSemantics.includes(descriptor.semantics)) {
82
98
  return await next();
83
99
  }
100
+ if (descriptor.semantics === 'put' || descriptor.semantics === 'delete') {
101
+ this.#useInvalidate(descriptor, ctx, next);
102
+ return;
103
+ }
84
104
  switch (descriptor.args.strategy) {
85
105
  case 'http': {
86
106
  await this.#useHTTP(descriptor, ctx, next);
@@ -99,24 +119,58 @@ export class CacheMiddleware {
99
119
  /**
100
120
  * @todo Implement vary rules.
101
121
  */
102
- #makeKey(descriptor) {
122
+ #makeKey(descriptor, ctx) {
123
+ const { authKey } = ctx;
103
124
  const { contentType } = descriptor;
104
125
  const { version } = descriptor.args;
105
126
  const { name } = descriptor.action;
106
127
  const { url } = descriptor.request;
107
- return 'v' + (version ?? 0) + '|' + name + '|' + contentType.toLowerCase() + '|' + url.toString();
128
+ if (authKey == null)
129
+ return 'v' + (version ?? 0) + '|' + name + '|' + contentType.toLowerCase() + '|' + url.toString();
130
+ return 'v' + (version ?? 0) + '|' + name + '|' + contentType.toLowerCase() + '|' + url.toString() + '|' + authKey;
108
131
  }
132
+ /**
133
+ * Sets response headers based of the cache args and authorization status.
134
+ */
109
135
  #setHeaders(descriptor, ctx) {
110
- const {} = descriptor.args;
136
+ const args = descriptor.args;
137
+ const cacheControl = [];
138
+ if (ctx.authKey != null && args.publicWhenAuthenticated) {
139
+ cacheControl.push('public');
140
+ }
141
+ else if (ctx.authKey != null || args.private) {
142
+ cacheControl.push('private');
143
+ }
144
+ else if (ctx.public) {
145
+ cacheControl.push('public');
146
+ }
147
+ if (cacheControl.length !== 0) {
148
+ ctx.headers.set('Cache-Control', cacheControl.join(', '));
149
+ }
150
+ }
151
+ /**
152
+ * Used for PUT and DELETE requests which should invalidate the cache
153
+ * when successful.
154
+ */
155
+ async #useInvalidate(descriptor, ctx, next) {
156
+ await next();
157
+ if (ctx.status != null || ctx.status.toString()[0] !== '2') {
158
+ return;
159
+ }
160
+ const key = this.#makeKey(descriptor, ctx);
161
+ const args = descriptor.args;
162
+ const cache = args.cache;
163
+ await cache.invalidate(key, ctx.url);
111
164
  }
112
165
  async #useHTTP(descriptor, ctx, next) {
113
166
  this.#setHeaders(descriptor, ctx);
114
167
  await next();
115
168
  }
116
169
  async #useEtag(descriptor, ctx, next) {
117
- const key = this.#makeKey(descriptor);
170
+ const key = this.#makeKey(descriptor, ctx);
118
171
  const rules = new EtagConditions(ctx.req.headers);
119
172
  const resourceState = await descriptor.args.cache.meta.get(key);
173
+ this.#setHeaders(descriptor, ctx);
120
174
  if (resourceState.type === 'cache-hit') {
121
175
  if (rules.ifMatch(resourceState.etag)) {
122
176
  return;
@@ -127,64 +181,78 @@ export class CacheMiddleware {
127
181
  return;
128
182
  }
129
183
  }
130
- this.#setHeaders(descriptor, ctx);
131
184
  await next();
132
185
  }
133
186
  async #useStore(descriptor, ctx, next) {
134
- const key = this.#makeKey(descriptor);
187
+ const key = this.#makeKey(descriptor, ctx);
135
188
  let resourceState;
136
- if (typeof descriptor.args.cache.meta.getOrLock === 'function' &&
137
- descriptor.args.lock) {
138
- try {
139
- resourceState = await descriptor.args.cache.meta.getOrLock(key);
140
- }
141
- catch (err) {
142
- resourceState = await descriptor.args.cache.meta.get(key);
189
+ const args = descriptor.args;
190
+ const cache = args.cache;
191
+ this.#setHeaders(descriptor, ctx);
192
+ if (descriptor.semantics === 'get' ||
193
+ descriptor.semantics === 'head' ||
194
+ descriptor.semantics === 'options' ||
195
+ descriptor.semantics === 'query') {
196
+ if (typeof cache.meta.getOrLock === 'function' &&
197
+ args.lock) {
198
+ try {
199
+ resourceState = await cache.meta.getOrLock(key);
200
+ }
201
+ catch (err) {
202
+ resourceState = await cache.meta.get(key);
203
+ }
143
204
  }
144
- }
145
- else {
146
- resourceState = await descriptor.args.cache.meta.get(key);
147
- }
148
- if (resourceState?.type === 'cache-hit') {
149
- if (this.#isNotModified(ctx.req.headers, resourceState.etag)) {
150
- ctx.hit = true;
151
- ctx.status = 304;
152
- return;
205
+ else {
206
+ resourceState = await cache.meta.get(key);
153
207
  }
154
- if (resourceState.hasContent) {
155
- try {
208
+ if (resourceState?.type === 'cache-hit') {
209
+ if (this.#isNotModified(ctx.req.headers, resourceState.etag)) {
156
210
  ctx.hit = true;
157
- ctx.status = resourceState.status;
158
- ctx.body = await descriptor.args.cache.storage.get(key);
159
- for (const [key, value] of resourceState.headers.entries()) {
160
- ctx.headers.set(key, value);
161
- }
211
+ ctx.status = 304;
162
212
  return;
163
213
  }
164
- catch (err) {
165
- console.log(err);
214
+ if (resourceState.hasContent) {
215
+ try {
216
+ ctx.hit = true;
217
+ ctx.status = resourceState.status;
218
+ ctx.body = await cache.storage.get(key);
219
+ for (const [key, value] of Object.entries(resourceState.headers)) {
220
+ if (Array.isArray(value)) {
221
+ ctx.headers.delete(key);
222
+ for (let i = 0; i < value.length; i++) {
223
+ ctx.headers.append(key, value[i]);
224
+ }
225
+ }
226
+ else {
227
+ ctx.headers.set(key, value);
228
+ }
229
+ }
230
+ return;
231
+ }
232
+ catch (err) {
233
+ console.log(err);
234
+ }
166
235
  }
167
236
  }
168
237
  }
169
238
  try {
170
- this.#setHeaders(descriptor, ctx);
171
239
  await next();
172
240
  const body = await new Response(ctx.body).blob();
173
241
  const etag = await this.#createEtag(body);
174
242
  ctx.etag = etag;
175
243
  ctx.headers.set('Etag', etag);
176
- await descriptor.args.cache.meta.set(key, {
244
+ await cache.meta.set(key, {
177
245
  key,
178
246
  authKey: ctx.authKey,
179
247
  iri: ctx.url,
180
248
  status: ctx.status ?? 200,
181
249
  hasContent: ctx.body != null,
182
- headers: ctx.headers,
250
+ headers: Object.fromEntries(ctx.headers.entries()),
183
251
  contentType: ctx.contentType,
184
252
  etag,
185
253
  });
186
254
  if (ctx.body != null) {
187
- await descriptor.args.cache.storage.set(key, body);
255
+ await cache.storage.set(key, body);
188
256
  }
189
257
  if (resourceState.type === 'locked-cache-miss') {
190
258
  await resourceState.release();
@@ -193,7 +261,6 @@ export class CacheMiddleware {
193
261
  ctx.hit = true;
194
262
  ctx.status = 304;
195
263
  ctx.body = null;
196
- ctx.headers.delete('Etag');
197
264
  return;
198
265
  }
199
266
  }
@@ -211,10 +278,12 @@ export class CacheMiddleware {
211
278
  const rules = new EtagConditions(headers);
212
279
  return rules.isNotModified(etag);
213
280
  }
214
- async #createEtag(body, weak = true) {
281
+ /**
282
+ * Creates a strong etag using a sha1 hashing algorithim.
283
+ */
284
+ async #createEtag(body) {
215
285
  const buff = await body.bytes();
216
286
  const hash = createHash('sha1').update(buff).digest('hex');
217
- const quoted = `"${hash}"`;
218
- return weak ? `W/${quoted}` : quoted;
287
+ return `"${hash}"`;
219
288
  }
220
289
  }
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
- import { EtagConditions } from './etag.js';
3
+ import { EtagConditions } from "./etag.js";
4
4
  test.describe('conditions.ifMatch()', () => {
5
5
  test('returns true when the header has the value of "*" and an etag is present', () => {
6
6
  const conditions = new EtagConditions(new Headers({