@jon49/sw 0.13.3 → 0.14.0

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.
@@ -0,0 +1,37 @@
1
+ import { SwResponse } from "./sw-framework.js"
2
+
3
+ function isHtmf(req: Request) {
4
+ return req.headers.has("HF-Request")
5
+ }
6
+
7
+ function htmfHeader(events: any = {}, messages: string[] = []) : Record<string, string> {
8
+ let userMessages =
9
+ messages?.length > 0
10
+ ? { "user-messages": messages }
11
+ : null
12
+ return {
13
+ "hf-events": JSON.stringify({
14
+ ...userMessages,
15
+ ...(events || {})
16
+ }) ?? "{}"
17
+ }
18
+ }
19
+
20
+ export function useHtmf(req: Request, res: SwResponse, ctx: any): void {
21
+ if (!isHtmf(req)) return
22
+
23
+ let messages = ctx.messages || []
24
+ if (res.error) {
25
+ messages.push(res.error)
26
+ res.error = undefined
27
+ }
28
+
29
+ let headers = htmfHeader(ctx.events, ctx.messages)
30
+
31
+ Object.assign(res, {
32
+ headers: {
33
+ ...res.headers,
34
+ ...headers,
35
+ }
36
+ })
37
+ }
@@ -0,0 +1,60 @@
1
+ import { isHtml } from "./utils.js"
2
+
3
+ const encoder = new TextEncoder()
4
+ function streamResponse(response: { body: Generator, headers?: any }): Response {
5
+ let { body, headers } = response
6
+ const stream = new ReadableStream({
7
+ async start(controller: ReadableStreamDefaultController<any>) {
8
+ try {
9
+ for await (let x of body) {
10
+ if (typeof x === "string")
11
+ controller.enqueue(encoder.encode(x))
12
+ }
13
+ controller.close()
14
+ } catch (error) {
15
+ console.error(error)
16
+ }
17
+ }
18
+ })
19
+
20
+ return new Response(stream, {
21
+ headers: {
22
+ "content-type": "text/html; charset=utf-8",
23
+ ...headers,
24
+ }
25
+ })
26
+ }
27
+
28
+ export function useResponse(_: Request, res: any, __: any) : void {
29
+ if (isHtml(res.body)) {
30
+ Object.assign(res, { response: streamResponse(res) })
31
+ return
32
+ }
33
+
34
+ if (res.error) {
35
+ Object.assign(res, {
36
+ response: new Response(res.error, {
37
+ status: 500,
38
+ headers: {
39
+ "content-type": "text/plain; charset=utf-8",
40
+ }
41
+ })
42
+ })
43
+ }
44
+
45
+ if (res.response) {
46
+ return res.response
47
+ }
48
+
49
+ if (res.body) {
50
+ Object.assign(res, {
51
+ response: new Response(res.body, {
52
+ status: res.status || 200,
53
+ headers: {
54
+ "content-type": res.type || "text/plain; charset=utf-8",
55
+ ...res.headers,
56
+ }
57
+ })
58
+ })
59
+ }
60
+ }
@@ -1,3 +1,6 @@
1
+ import { SwResponse } from "./sw-framework.js"
2
+ import { isHtml } from "./utils.js"
3
+
1
4
  let { links, globalDb } =
2
5
  // @ts-ignore
3
6
  self.app as { links: { file: string, url: string }[], html: Function, db: any, globalDb: any }
@@ -29,48 +32,18 @@ interface ResponseOptions {
29
32
 
30
33
  export let options: ResponseOptions = {}
31
34
 
32
- // Test if value is Async Generator
33
- let isHtml = (value: any) =>
34
- value?.next instanceof Function
35
- && value.throw instanceof Function
36
-
37
35
  // @ts-ignore
38
- export async function getResponse(event: FetchEvent): Promise<Response> {
36
+ export async function useRoutes(req: Request, res: SwResponse, ctx: any): Promise<void> {
39
37
  try {
40
- const req: Request = event.request
41
38
  const url = normalizeUrl(req.url)
42
- return (
43
- !url.pathname.startsWith("/web/")
44
- ? fetch(req)
45
- : executeHandler({ url, req, event }))
39
+ if (!url.pathname.startsWith("/web/")) {
40
+ res.response = await fetch(req)
41
+ } else {
42
+ Object.assign(res, await executeHandler({ url, req, ctx }))
43
+ }
46
44
  } catch (error) {
47
45
  console.error("Get Response Error", error)
48
- return new Response("Oops something happened which shouldn't have!")
49
- }
50
- }
51
-
52
- function getErrors(errors: any): string[] {
53
- return typeof errors === "string"
54
- ? [errors]
55
- : call(options.handleErrors, errors) ?? []
56
- }
57
-
58
- function isHtmf(req: Request) {
59
- return req.headers.has("HF-Request")
60
- }
61
-
62
- function htmfHeader(req: Request, events: any = {}, messages: string[] = [])
63
- : Record<string, string> {
64
- if (!isHtmf(req)) return {}
65
- let userMessages =
66
- messages?.length > 0
67
- ? { "user-messages": messages }
68
- : null
69
- return {
70
- "hf-events": JSON.stringify({
71
- ...userMessages,
72
- ...(events || {})
73
- }) ?? "{}"
46
+ res.error = "Oops something happened which shouldn't have!"
74
47
  }
75
48
  }
76
49
 
@@ -139,15 +112,14 @@ export async function findRoute(url: URL, method: unknown) {
139
112
  interface ExectuteHandlerOptions {
140
113
  url: URL
141
114
  req: Request
142
- // @ts-ignore
143
- event: FetchEvent
115
+ ctx: any
144
116
  }
145
- async function executeHandler({ url, req, event }: ExectuteHandlerOptions): Promise<Response> {
117
+ async function executeHandler({ url, req, ctx }: ExectuteHandlerOptions): Promise<SwResponse> {
146
118
  let method = req.method.toLowerCase()
147
119
  let isPost = method === "post"
148
120
  if (!isPost) {
149
121
  if (!url.pathname.endsWith("/")) {
150
- return cacheResponse(url.pathname, event)
122
+ return { response: await cacheResponse(url.pathname, req) }
151
123
  }
152
124
 
153
125
  if (url.searchParams.get("login") === "success") {
@@ -175,71 +147,57 @@ async function executeHandler({ url, req, event }: ExectuteHandlerOptions): Prom
175
147
 
176
148
  if (!result) {
177
149
  return isPost
178
- ? redirect(req)
179
- : new Response("Not Found!", { status: 404 })
150
+ ? { response: redirect(req) }
151
+ : { error: "Not found!" }
180
152
  }
181
153
 
182
154
  if (isPost && result.message == null) {
183
155
  messages.push("Saved!")
184
156
  }
185
157
 
186
- if (isHtml(result)) {
187
- return streamResponse({
188
- body: result,
189
- headers: htmfHeader(req, null, messages)
190
- })
191
- }
192
-
193
158
  if (result.message?.length > 0) {
194
159
  messages.push(result.message)
195
160
  } else if (result.messages?.length > 0) {
196
161
  messages.push(...result.messages)
197
162
  }
163
+ ctx.messages = messages
164
+ ctx.events = result.events
198
165
 
199
- result.headers = {
200
- ...htmfHeader(req, result.events, messages),
201
- ...result.headers
166
+ if (isHtml(result)) {
167
+ return {
168
+ body: result,
169
+ }
202
170
  }
203
171
 
204
172
  if (isHtml(result.body)) {
205
- return streamResponse(result)
173
+ return {
174
+ ...result,
175
+ type: "text/html",
176
+ }
206
177
  } else {
207
178
  if ("json" in result) {
208
179
  result.body = JSON.stringify(result.json)
209
180
  result.headers = {
210
181
  ...result.headers,
211
- "content-type": "application/json"
212
182
  }
213
183
  }
214
- return new Response(result.body, {
215
- status: result.status ?? 200,
216
- headers: result.headers
217
- })
184
+ return {
185
+ ...result,
186
+ type: "application/json"
187
+ }
218
188
  }
219
189
 
220
190
  } catch (error) {
221
191
  console.error(`"${method}" error:`, error, "\nURL:", url);
222
- if (!isHtmf(req)) {
223
- if (isPost) {
224
- return redirect(req)
225
- } else {
226
- return new Response("Not Found!", { status: 404 })
227
- }
192
+ if (isPost) {
193
+ return { error: "An error occurred while processing your request." }
228
194
  } else {
229
- let errors: string[] = getErrors(error)
230
- let headers = htmfHeader(req, {}, errors)
231
- return new Response(null, {
232
- status: isPost ? 400 : 500,
233
- headers
234
- })
195
+ return { error: "Not found!" }
235
196
  }
236
197
  }
237
198
  }
238
199
 
239
- return new Response(null, {
240
- status: 404,
241
- headers: htmfHeader(req, {}, ["Not Found!"])
242
- })
200
+ return { error: "Not Found!" }
243
201
  }
244
202
 
245
203
  async function getData(req: Request) {
@@ -264,46 +222,6 @@ async function getData(req: Request) {
264
222
  return o
265
223
  }
266
224
 
267
- async function cacheResponse(url: string, event?: { request: string | Request } | undefined): Promise<Response> {
268
- url = links?.find(x => x.url === url)?.file || url
269
- const match = await caches.match(url)
270
- if (match) return match
271
- const res = await fetch(event?.request || url)
272
- if (!res || res.status !== 200 || res.type !== "basic") return res
273
- const responseToCache = res.clone()
274
- // @ts-ignore
275
- let version: string = self.app?.version
276
- ?? (console.warn("The version number is not available, expected glboal value `self.app.version`."), "")
277
- const cache = await caches.open(version)
278
- cache.put(url, responseToCache)
279
- return res
280
- }
281
-
282
- const encoder = new TextEncoder()
283
- function streamResponse(response: { body: Generator, headers?: any }): Response {
284
- let { body, headers } = response
285
- const stream = new ReadableStream({
286
- async start(controller: ReadableStreamDefaultController<any>) {
287
- try {
288
- for await (let x of body) {
289
- if (typeof x === "string")
290
- controller.enqueue(encoder.encode(x))
291
- }
292
- controller.close()
293
- } catch (error) {
294
- console.error(error)
295
- }
296
- }
297
- })
298
-
299
- return new Response(stream, {
300
- headers: {
301
- "content-type": "text/html; charset=utf-8",
302
- ...headers,
303
- }
304
- })
305
- }
306
-
307
225
  /**
308
226
  * /my/url -> /my/url/
309
227
  * /my/script.js -> /my/script.js
@@ -319,6 +237,21 @@ function isFile(s: string) {
319
237
  return s.lastIndexOf("/") < s.lastIndexOf(".")
320
238
  }
321
239
 
240
+ async function cacheResponse(url: string, req?: Request | undefined): Promise<Response> {
241
+ url = links?.find(x => x.url === url)?.file || url
242
+ const match = await caches.match(url)
243
+ if (match) return match
244
+ const res = await fetch(req || url)
245
+ if (!res || res.status !== 200 || res.type !== "basic") return res
246
+ const responseToCache = res.clone()
247
+ // @ts-ignore
248
+ let version: string = self.app?.version
249
+ ?? (console.warn("The version number is not available, expected glboal value `self.app.version`."), "")
250
+ const cache = await caches.open(version)
251
+ cache.put(url, responseToCache)
252
+ return res
253
+ }
254
+
322
255
  export interface RouteGetArgs {
323
256
  req: Request
324
257
  query: any
@@ -0,0 +1,53 @@
1
+ class SWFramework {
2
+ private middlewares: Middleware[];
3
+
4
+ constructor() {
5
+ this.middlewares = [];
6
+ }
7
+
8
+ use(middleware: Middleware) {
9
+ if (typeof middleware !== 'function') {
10
+ throw new Error('Middleware must be a function');
11
+ }
12
+ this.middlewares.push(middleware);
13
+ }
14
+
15
+ async start(fetchEvent: FetchEvent) : Promise<Response> {
16
+ let ctx = {} // Context object to pass to middlewares
17
+ let res: SwResponse = {}
18
+ let req = fetchEvent.request
19
+ for (let middleware of this.middlewares) {
20
+ let result = middleware(req, res, ctx)
21
+ if (result instanceof Promise) {
22
+ result = await result;
23
+ }
24
+ if (result === false) {
25
+ // Stop processing middlewares if one returns false
26
+ break;
27
+ }
28
+ }
29
+ return res.response ?? new Response(res.error || "Not Found", {
30
+ status: res.status || 404,
31
+ headers: {
32
+ "content-type": res.type || "text/plain; charset=utf-8",
33
+ ...res.headers,
34
+ }
35
+ })
36
+ }
37
+ }
38
+
39
+ export let swFramework = new SWFramework();
40
+
41
+ export type SwResponse = {
42
+ status?: number
43
+ headers?: { [key: string]: string }
44
+ response?: Response
45
+ error?: string
46
+ } & (
47
+ | { body?: never; type?: never }
48
+ | { body: any; type: "text/plain" | "application/json" | "text/html" | "application/xml" }
49
+ );
50
+
51
+ interface Middleware {
52
+ (req: Request, res: SwResponse, ctx: any): void | Promise<void | false> | false
53
+ }
package/lib/utils.ts CHANGED
@@ -24,3 +24,9 @@ export class DbCache {
24
24
  }
25
25
  }
26
26
 
27
+ // Test if value is Async Generator
28
+ export function isHtml(value: any) {
29
+ return value?.next instanceof Function
30
+ && value.throw instanceof Function
31
+ }
32
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jon49/sw",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "description": "Packages for MVC service workers.",
5
5
  "type": "module",
6
6
  "files": [
@@ -8,7 +8,10 @@
8
8
  "bin"
9
9
  ],
10
10
  "exports": {
11
- "./routes.js": "./lib/routes.ts",
11
+ "./routes.middleware.js": "./lib/routes.middleware.ts",
12
+ "./htmf.middleware.js": "./lib/htmf.middleware.ts",
13
+ "./response.middleware.js": "./lib/response.middleware.ts",
14
+ "./web-framework.js": "./lib/sw-framework.ts",
12
15
  "./new-app-notifier.js": "./lib/new-app-notifier.ts",
13
16
  "./validation.js": "./lib/validation.ts",
14
17
  "./utils.js": "./lib/utils.ts",