UNPKG

8.46 kBPlain TextView Raw
1// deno-lint-ignore-file no-explicit-any
2export { pipe as combine } from 'ts-functional-pipe';
3import { ResolvablePromise } from '@worker-tools/resolvable-promise'
4
5import { AppendOnlyList } from "./utils/append-only-list.js";
6import { Awaitable, Callable } from "./utils/common-types.js";
7
8export type ResponseEffect = (r: Response) => void | Awaitable<Response>
9
10export class EffectsList extends AppendOnlyList<ResponseEffect> {}
11
12export interface Context {
13 /**
14 * The original request for use in middleware. Also accessible via first argument to user handler.
15 */
16 request: Request,
17
18 /**
19 * A list of effects/transforms applied to the `Response` after the application handler completes.
20 * Middleware can add effects to the list. Application handlers should ignore it.
21 * @deprecated Prop might change name
22 */
23 effects: AppendOnlyList<ResponseEffect>,
24
25 /**
26 * TODO
27 */
28 waitUntil: (f: any) => void,
29
30 /**
31 * A promise that resolves when middleware is done applying effects.
32 * Related: https://github.com/w3c/ServiceWorker/issues/1397
33 */
34 handled: Promise<Response>
35
36 /**
37 * The URL pattern match that caused this handler to run. See the URL Pattern API for more.
38 */
39 match?: URLPatternResult,
40
41 /**
42 * Only available if the router is used via `fetchEventListener`.
43 * Many Worker Runtimes such as Deno an CF module workers don't provide fetch events.
44 */
45 event?: FetchEvent,
46
47 /** Might be present based on usage */
48 env?: any
49
50 /** Might be present based on usage */
51 ctx?: any
52
53 /** Might be present based on usage */
54 connInfo?: any
55
56 /** Might be present based on usage */
57 args?: any[]
58}
59
60/**
61 * Helper type to get the context type of a given middleware function.
62 *
63 * Example:
64 * ```ts
65 * const mw = combine(basics(), contentTypes(['text/html', 'application/json']))
66 * type MWContext = ContextOf<typeof mw>;
67 * const handler = (req: Request, context: MWContext) => ok()
68 * new WorkerRouter().get('/', mw, handler)
69 * ```
70 */
71export type ContextOf<MW extends (...args: any[]) => Awaitable<Context>> = Awaited<ReturnType<MW>>
72
73/**
74 * @deprecated Function might change name
75 */
76export function executeEffects(effects: EffectsList, response: Awaitable<Response>) {
77 // TODO: to reduce or reduceRight, that is the question...
78 // reduceRight matches the behavior of my initial, non-compose friendly middleware model
79 // which was just increasingly deep levels of wrapped function calls.
80 // In that model, the effects (post-processes) of the last applied middleware were executed first.
81 // Regular reduce matches the order in which middlewares are applied,
82 // which probably is close what users expect to happen, anyway...
83 return [...effects].reduceRight(async (response, effect) => effect(await response) ?? response, response) ?? response
84}
85
86/** Any record of unknown values */
87export type Rec = Record<PropertyKey, any>
88
89/**
90 * A helper function to create user-defined middleware.
91 *
92 * Its main purpose is to allow developers to create correctly typed middleware without dealing with generics.
93 * This is achieved via the `_defaultExt` parameter, which is used to infer the types of the *extension* added to the *context*.
94 * As the `_` prefix implies, it is not actually used.
95 * The purpose of the default extension object is solely to tell the type checker which additional keys to expect on the context object after this middleware is applied.
96 * The job of adding (default) values to the context belongs to the middleware function.
97 *
98 * Here are some example usages. All are valid in JavaScript and TypeScript:
99 *
100 * ```ts
101 * const fn = createMiddleware({}, _ => _)
102 * const gn = createMiddleware({}, async ax => ({ ...await ax }))
103 * const hn = createMiddleware({ foo: '' }, async ax => ({ ...await ax, foo: 'star' }))
104 * const jn = createMiddleware({ bar: '' }, async ax => {
105 * const x = await ax;
106 * x.effects.push(resp => {
107 * resp.headers.set('x-middleware', 'jn')
108 * })
109 * return { ...x, bar: 'star' }
110 * })
111 * const myMW = combine(fn, hn, jn, gn)
112 * //=> Context & { foo: string } & { bar: string }
113 * ```
114 *
115 * @param _defaultExt The default extension to the current context. Can also be a function that returns the extension object, to avoid unnecessary memory allocation.
116 * @param middlewareFn A middleware functions: Adds the keys listed in `defaultExt` to the context
117 * @returns The provided `middlewareFn` with type annotations inferred based on `defaultExt`
118 */
119export function createMiddleware<Etx extends Rec>(_defaultExt: Callable<Etx>, middlewareFn: <Ctx extends Context>(ax: Awaitable<Ctx>) => Awaitable<Ctx & Etx>) {
120 return middlewareFn;
121}
122
123/** @deprecated Name might change */
124export type ErrorContext = Context & { error: Error, response: Response }
125/** @deprecated Name might change */
126export type Handler<X extends Context> = (request: Request, ctx: X) => Awaitable<Response>;
127/** @deprecated Name might change */
128export type ErrorHandler<X extends ErrorContext> = (request: Request, ctx: X) => Awaitable<Response>;
129/** @deprecated Name might change */
130export type Middleware<X extends Context, Y extends Context> = (x: Awaitable<X>) => Awaitable<Y>;
131
132/**
133 * Takes a handler function of the form `(x: Request, ctx: Context) => Awaitable<Response>` and applies middleware to it.
134 * @deprecated Name might change, errorHandler not implemented
135 */
136export function withMiddleware<X extends Context, EX extends ErrorContext>(middleware: Middleware<Context, X>, handler: Handler<X>, _errorHandler?: ErrorHandler<EX>) {
137 return async (request: Request, ...args: any[]) => {
138 const handle = new ResolvablePromise<Response>()
139 const handled = Promise.resolve(handle);
140 const effects = new EffectsList();
141 const ctx = { request, effects, handled, args: [request, ...args], waitUntil: () => {} };
142 try {
143 const userCtx = await middleware(ctx);
144 const userRes = await handler(request, userCtx)
145 const response = await executeEffects(effects, userRes)
146 handle.resolve(response)
147 return response;
148 } catch (err) {
149 throw err
150 // TODO
151 // if (fallback && err instanceof Response) {
152 // fallback(request, Object.assign(ctx, { response: err }))
153 // }
154 }
155 }
156}
157
158/**
159 * Extends the lifetime of the install and activate events dispatched on the global scope as part of the
160 * service worker lifecycle. This ensures that any functional events (like FetchEvent) are not dispatched until it
161 * upgrades database schemas and deletes the outdated cache entries.
162 */
163export interface ExtendableEvent extends Event {
164 waitUntil(f: any): void;
165}
166
167export interface ExtendableEventInit extends EventInit {
168 new(type: string, eventInitDict?: ExtendableEventInit): ExtendableEvent;
169}
170
171export interface FetchEventInit extends ExtendableEventInit {
172 new(type: string, eventInitDict: FetchEventInit): FetchEvent;
173 clientId?: string;
174 preloadResponse?: Promise<any>;
175 replacesClientId?: string;
176 request: Request;
177 resultingClientId?: string;
178}
179
180/**
181 * This is the event type for fetch events dispatched on the service worker global scope.
182 * It contains information about the fetch, including the request and how the receiver will treat the response.
183 * It provides the event.respondWith() method, which allows us to provide a response to this fetch.
184 */
185export interface FetchEvent extends ExtendableEvent {
186 readonly clientId: string;
187 readonly preloadResponse: Promise<any>;
188 readonly replacesClientId: string;
189 readonly request: Request;
190 readonly resultingClientId: string;
191 readonly handled: Promise<void>;
192 respondWith(r: Response | Promise<Response>): void;
193}
194
195export type URLPatternInput = URLPatternInit | string;
196
197export interface URLPatternInit {
198 baseURL?: string;
199 username?: string;
200 password?: string;
201 protocol?: string;
202 hostname?: string;
203 port?: string;
204 pathname?: string;
205 search?: string;
206 hash?: string;
207}
208
209export interface URLPatternResult {
210 inputs: [URLPatternInit] | [URLPatternInit, string];
211 protocol: URLPatternComponentResult;
212 username: URLPatternComponentResult;
213 password: URLPatternComponentResult;
214 hostname: URLPatternComponentResult;
215 port: URLPatternComponentResult;
216 pathname: URLPatternComponentResult;
217 search: URLPatternComponentResult;
218 hash: URLPatternComponentResult;
219}
220
221export interface URLPatternComponentResult {
222 input: string;
223 groups: {
224 [key: string]: string | undefined;
225 };
226}