@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.
- package/dist/accept.js +0 -1
- package/dist/actions/actionSets.d.ts +3 -3
- package/dist/actions/actions.d.ts +69 -48
- package/dist/actions/actions.js +39 -4
- package/dist/actions/context.d.ts +15 -11
- package/dist/actions/context.js +5 -0
- package/dist/actions/meta.d.ts +18 -12
- package/dist/actions/meta.js +114 -38
- package/dist/actions/spec.d.ts +3 -3
- package/dist/actions/types.d.ts +45 -16
- package/dist/actions/writer.d.ts +1 -1
- package/dist/actions/writer.test.js +2 -2
- package/dist/cache/cache.d.ts +3 -3
- package/dist/cache/cache.js +111 -42
- package/dist/cache/etag.test.js +1 -1
- package/dist/cache/file.d.ts +33 -1
- package/dist/cache/file.js +92 -10
- package/dist/cache/memory.d.ts +12 -2
- package/dist/cache/memory.js +63 -1
- package/dist/cache/types.d.ts +51 -22
- package/dist/errors.d.ts +1 -1
- package/dist/jsonld.d.ts +1 -1
- package/dist/makeTypeDefs.d.ts +2 -2
- package/dist/mod.d.ts +17 -15
- package/dist/mod.js +17 -15
- package/dist/processAction.d.ts +2 -2
- package/dist/processAction.js +1 -1
- package/dist/registry.d.ts +74 -8
- package/dist/registry.js +70 -8
- package/dist/registry.test.js +1 -1
- package/dist/scopes.d.ts +8 -8
- package/dist/scopes.js +8 -5
- package/dist/utils/contextBuilder.d.ts +1 -1
- package/dist/utils/getActionContext.d.ts +2 -2
- package/dist/utils/getPropertyValueSpecifications.d.ts +1 -1
- package/dist/utils/getRequestBodyValues.d.ts +3 -3
- package/dist/utils/getRequestIRIValues.d.ts +2 -2
- package/dist/utils/isPopulatedObject.js +1 -1
- package/dist/utils/makeAppendProblemDetails.d.ts +1 -1
- package/dist/utils/makeURLPattern.js +1 -0
- package/dist/utils/parseSearchParams.d.ts +2 -2
- package/dist/validators.d.ts +2 -2
- package/dist/validators.js +2 -2
- package/lib/accept.test.ts +1 -1
- package/lib/accept.ts +0 -2
- package/lib/actions/actionSets.ts +4 -4
- package/lib/actions/actions.ts +159 -99
- package/lib/actions/context.ts +22 -10
- package/lib/actions/meta.ts +140 -55
- package/lib/actions/path.test.ts +1 -1
- package/lib/actions/path.ts +1 -1
- package/lib/actions/spec.ts +3 -3
- package/lib/actions/types.ts +60 -15
- package/lib/actions/writer.test.ts +2 -2
- package/lib/actions/writer.ts +1 -1
- package/lib/cache/cache.ts +138 -52
- package/lib/cache/etag.test.ts +1 -1
- package/lib/cache/file.ts +113 -12
- package/lib/cache/memory.ts +85 -3
- package/lib/cache/types.ts +70 -23
- package/lib/errors.ts +1 -1
- package/lib/jsonld.ts +1 -1
- package/lib/makeTypeDefs.ts +5 -5
- package/lib/mod.ts +17 -15
- package/lib/processAction.ts +14 -14
- package/lib/registry.test.ts +1 -1
- package/lib/registry.ts +96 -19
- package/lib/request.ts +1 -1
- package/lib/scopes.test.ts +2 -2
- package/lib/scopes.ts +14 -11
- package/lib/utils/contextBuilder.ts +3 -3
- package/lib/utils/getActionContext.ts +4 -4
- package/lib/utils/getInternalName.ts +1 -1
- package/lib/utils/getPropertyValueSpecifications.ts +4 -4
- package/lib/utils/getRequestBodyValues.ts +5 -5
- package/lib/utils/getRequestIRIValues.ts +4 -4
- package/lib/utils/isPopulatedObject.ts +1 -1
- package/lib/utils/makeAppendProblemDetails.ts +1 -1
- package/lib/utils/makeURLPattern.ts +1 -0
- package/lib/utils/parseSearchParams.ts +2 -2
- package/lib/validators.ts +5 -5
- package/package.json +4 -2
package/dist/actions/meta.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
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.
|
|
86
|
+
headers.append('Server-Timing', `${name};dur=${duration.toFixed(2)}`);
|
|
54
87
|
prevTime = nextTime;
|
|
55
88
|
};
|
|
56
|
-
if (
|
|
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 (
|
|
103
|
+
if (performServerTiming)
|
|
71
104
|
serverTiming('handle');
|
|
72
105
|
};
|
|
73
106
|
{
|
|
74
107
|
const upstream = next;
|
|
75
108
|
next = async () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
|
90
|
-
query:
|
|
91
|
-
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 (
|
|
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 (
|
|
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
|
}
|
package/dist/actions/spec.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { JSONPrimitive, JSONValue, OrArray, RecursiveDigit, RecursiveIncrement, TypeDef } from "../jsonld.
|
|
2
|
-
import { Action } from "./actions.
|
|
3
|
-
import type { Context } from './context.
|
|
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>;
|
package/dist/actions/types.d.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
import type { HTTPWriter } from "./writer.
|
|
2
|
-
import type { Registry } from '../registry.
|
|
3
|
-
import type { Scope } from "../scopes.
|
|
4
|
-
import type { ContextState, ActionSpec } from "./spec.
|
|
5
|
-
import type { Context } from "./context.
|
|
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.
|
|
8
|
-
import type { HandlerDefinition } from "../mod.
|
|
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
|
*
|
package/dist/actions/writer.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
19
|
+
href: 'https://example.com/main.ts',
|
|
20
20
|
as: 'script',
|
|
21
21
|
preload: true,
|
|
22
22
|
fetchPriority: 'low',
|
package/dist/cache/cache.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CacheContext, NextFn, Registry } from '../mod.
|
|
2
|
-
import type { CacheBuilder, CacheEntryDescriptor, CacheETagArgs, CacheETagInstanceArgs, CacheHTTPArgs, CacheHTTPInstanceArgs, CacheMeta, CacheStorage, CacheStoreArgs, CacheStoreInstanceArgs, UpstreamCache } from './types.
|
|
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(
|
|
25
|
+
invalidate(key: string, url: string): Promise<void>;
|
|
26
26
|
}
|
|
27
27
|
export declare class CacheMiddleware {
|
|
28
28
|
#private;
|
package/dist/cache/cache.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { EtagConditions } from
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
208
|
+
if (resourceState?.type === 'cache-hit') {
|
|
209
|
+
if (this.#isNotModified(ctx.req.headers, resourceState.etag)) {
|
|
156
210
|
ctx.hit = true;
|
|
157
|
-
ctx.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
|
-
|
|
165
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
return weak ? `W/${quoted}` : quoted;
|
|
287
|
+
return `"${hash}"`;
|
|
219
288
|
}
|
|
220
289
|
}
|
package/dist/cache/etag.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import { EtagConditions } from
|
|
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({
|