@pikku/core 0.9.5 → 0.9.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 (70) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/function/function-runner.d.ts +2 -2
  3. package/dist/function/function-runner.js +3 -3
  4. package/dist/function/functions.types.d.ts +4 -0
  5. package/dist/function/functions.types.js +6 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +3 -3
  8. package/dist/middleware-runner.d.ts +5 -4
  9. package/dist/middleware-runner.js +20 -6
  10. package/dist/permissions.d.ts +3 -3
  11. package/dist/permissions.js +30 -8
  12. package/dist/pikku-state.d.ts +2 -4
  13. package/dist/pikku-state.js +3 -2
  14. package/dist/schema.js +8 -6
  15. package/dist/types/core.types.d.ts +4 -0
  16. package/dist/types/core.types.js +6 -0
  17. package/dist/utils.d.ts +1 -0
  18. package/dist/utils.js +17 -0
  19. package/dist/wirings/channel/channel-handler.js +2 -1
  20. package/dist/wirings/channel/channel-runner.d.ts +2 -0
  21. package/dist/wirings/channel/channel-runner.js +23 -25
  22. package/dist/wirings/channel/local/local-channel-runner.js +5 -3
  23. package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -2
  24. package/dist/wirings/http/http-runner.js +36 -53
  25. package/dist/wirings/http/http.types.d.ts +1 -1
  26. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  27. package/dist/wirings/http/routers/http-router.d.ts +12 -0
  28. package/dist/wirings/http/routers/http-router.js +2 -0
  29. package/dist/wirings/http/routers/path-to-regex.d.ts +10 -0
  30. package/dist/wirings/http/routers/path-to-regex.js +118 -0
  31. package/dist/wirings/mcp/mcp-runner.d.ts +1 -1
  32. package/dist/wirings/mcp/mcp-runner.js +3 -2
  33. package/dist/wirings/queue/queue-runner.js +3 -2
  34. package/dist/wirings/rpc/rpc-runner.js +2 -1
  35. package/dist/wirings/scheduler/scheduler-runner.d.ts +1 -1
  36. package/dist/wirings/scheduler/scheduler-runner.js +3 -2
  37. package/package.json +1 -1
  38. package/src/factory-functions.test.ts +37 -0
  39. package/src/function/function-runner.test.ts +101 -52
  40. package/src/function/function-runner.ts +5 -2
  41. package/src/function/functions.types.ts +13 -0
  42. package/src/index.ts +3 -11
  43. package/src/middleware-runner.test.ts +71 -43
  44. package/src/middleware-runner.ts +43 -17
  45. package/src/permissions.test.ts +21 -17
  46. package/src/permissions.ts +68 -26
  47. package/src/pikku-state.ts +5 -3
  48. package/src/schema.ts +8 -6
  49. package/src/types/core.types.ts +12 -0
  50. package/src/utils.ts +19 -0
  51. package/src/wirings/channel/channel-handler.ts +17 -11
  52. package/src/wirings/channel/channel-runner.ts +31 -25
  53. package/src/wirings/channel/local/local-channel-runner.test.ts +4 -0
  54. package/src/wirings/channel/local/local-channel-runner.ts +5 -4
  55. package/src/wirings/channel/serverless/serverless-channel-runner.ts +13 -11
  56. package/src/wirings/http/http-runner.test.ts +18 -6
  57. package/src/wirings/http/http-runner.ts +55 -74
  58. package/src/wirings/http/http.types.ts +1 -1
  59. package/src/wirings/http/pikku-fetch-http-request.ts +3 -0
  60. package/src/wirings/http/routers/http-router.ts +15 -0
  61. package/src/wirings/http/routers/path-to-regex.test.ts +309 -0
  62. package/src/wirings/http/routers/path-to-regex.ts +162 -0
  63. package/src/wirings/mcp/mcp-runner.ts +18 -12
  64. package/src/wirings/queue/queue-runner.ts +15 -7
  65. package/src/wirings/rpc/rpc-runner.ts +21 -16
  66. package/src/wirings/scheduler/scheduler-runner.ts +18 -12
  67. package/tsconfig.tsbuildinfo +1 -1
  68. package/dist/pikku-function.d.ts +0 -1
  69. package/dist/pikku-function.js +0 -1
  70. package/src/pikku-function.ts +0 -1
@@ -12,8 +12,8 @@ import {
12
12
  CoreUserSession,
13
13
  CorePikkuMiddleware,
14
14
  SessionServices,
15
+ PikkuWiringTypes,
15
16
  } from '../../types/core.types.js'
16
- import { match } from 'path-to-regexp'
17
17
  import { MissingSessionError, NotFoundError } from '../../errors/errors.js'
18
18
  import {
19
19
  closeSessionServices,
@@ -32,6 +32,7 @@ import { PikkuFetchHTTPRequest } from './pikku-fetch-http-request.js'
32
32
  import { PikkuChannel } from '../channel/channel.types.js'
33
33
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
34
34
  import { rpcService } from '../rpc/rpc-runner.js'
35
+ import { httpRouter } from './routers/http-router.js'
35
36
 
36
37
  /**
37
38
  * Registers middleware either globally or for a specific route.
@@ -49,32 +50,20 @@ export const addHTTPMiddleware = <PikkuMiddleware extends CorePikkuMiddleware>(
49
50
  middleware?: PikkuMiddleware[]
50
51
  ) => {
51
52
  const middlewareStore = pikkuState('http', 'middleware')
53
+ let route = '*'
52
54
 
53
- if (typeof routeOrMiddleware === 'string' && middleware) {
54
- const route = routeOrMiddleware
55
-
56
- // Remove existing entry for this route if it exists
57
- const existingIndex = middlewareStore.findIndex(
58
- (item) => item.route === route
59
- )
60
- if (existingIndex !== -1) {
61
- middlewareStore.splice(existingIndex, 1)
62
- }
63
-
64
- middlewareStore.push({ route, middleware })
55
+ if (typeof routeOrMiddleware === 'string') {
56
+ route = routeOrMiddleware
57
+ middleware = middleware!
65
58
  } else {
66
- // Handle global middleware
67
- const existingIndex = middlewareStore.findIndex(
68
- (item) => item.route === '*'
69
- )
70
- if (existingIndex !== -1) {
71
- middlewareStore.splice(existingIndex, 1)
72
- }
59
+ middleware = routeOrMiddleware
60
+ }
73
61
 
74
- middlewareStore.push({
75
- route: '*',
76
- middleware: routeOrMiddleware as any,
77
- })
62
+ const currentMiddleware = middlewareStore.get(route)
63
+ if (currentMiddleware) {
64
+ middlewareStore.set(route, [...currentMiddleware, ...middleware])
65
+ } else {
66
+ middlewareStore.set(route, middleware)
78
67
  }
79
68
  }
80
69
 
@@ -113,10 +102,7 @@ export const wireHTTP = <
113
102
  >
114
103
  ) => {
115
104
  const httpMeta = pikkuState('http', 'meta')
116
- const routeMeta = httpMeta.find(
117
- (meta) =>
118
- meta.route === httpWiring.route && meta.method === httpWiring.method
119
- )
105
+ const routeMeta = httpMeta[httpWiring.method][httpWiring.route]
120
106
  if (!routeMeta) {
121
107
  throw new Error('Route metadata not found')
122
108
  }
@@ -142,43 +128,29 @@ export const wireHTTP = <
142
128
  * @returns {Object | undefined} An object with matched route details or undefined if no match.
143
129
  */
144
130
  const getMatchingRoute = (requestType: string, requestPath: string) => {
145
- const allRoutes = pikkuState('http', 'routes')
146
- const middleware = pikkuState('http', 'middleware')
147
- const routes = allRoutes.get(requestType.toLowerCase() as HTTPMethod)
148
- if (!routes) {
149
- return undefined
150
- }
151
- for (const route of routes.values()) {
152
- // Generate a matching function from the route pattern
153
- const matchFunc = match(`/${route.route}`.replace(/^\/\//, '/'), {
154
- decode: decodeURIComponent,
155
- })
156
-
157
- // Attempt to match the request path
158
- const matchedPath = matchFunc(requestPath.replace(/^\/\//, '/'))
159
-
160
- if (matchedPath) {
161
- // Aggregate global and route-specific middleware
162
- const globalMiddleware = middleware
163
- .filter((m) => m.route === '*' || new RegExp(m.route).test(route.route))
164
- .map((m) => m.middleware)
165
- .flat()
166
-
167
- const meta = pikkuState('http', 'meta').find(
168
- (meta) => meta.route === route.route && meta.method === route.method
169
- )
131
+ const matchedPath = httpRouter.match(
132
+ requestType.toLowerCase() as HTTPMethod,
133
+ requestPath
134
+ )
170
135
 
171
- return {
172
- matchedPath,
173
- params: matchedPath.params,
174
- route,
175
- permissions: route.permissions,
176
- middleware: [...globalMiddleware, ...(route.middleware || [])],
177
- meta: meta!,
178
- }
136
+ if (matchedPath) {
137
+ const route = pikkuState('http', 'routes')
138
+ .get(requestType.toLowerCase() as HTTPMethod)!
139
+ .get(matchedPath.route)!
140
+ const meta = pikkuState('http', 'meta')[
141
+ requestType.toLowerCase() as PikkuWiringTypes
142
+ ][route.route]
143
+
144
+ return {
145
+ matchedPath,
146
+ params: matchedPath.params,
147
+ route,
148
+ permissions: route.permissions,
149
+ httpMiddleware: matchedPath.middleware,
150
+ middleware: route.middleware,
151
+ meta: meta!,
179
152
  }
180
153
  }
181
- return undefined
182
154
  }
183
155
 
184
156
  /**
@@ -251,7 +223,8 @@ const executeRouteWithMiddleware = async (
251
223
  matchedPath: any
252
224
  params: any
253
225
  route: CoreHTTPFunctionWiring<any, any, any>
254
- middleware: any[]
226
+ httpMiddleware: CorePikkuMiddleware[] | undefined
227
+ middleware: CorePikkuMiddleware[] | undefined
255
228
  meta: HTTPWiringMeta
256
229
  },
257
230
  http: PikkuHTTP,
@@ -259,7 +232,8 @@ const executeRouteWithMiddleware = async (
259
232
  coerceDataFromSchema: boolean
260
233
  }
261
234
  ) => {
262
- const { matchedPath, params, route, middleware, meta } = matchedRoute
235
+ const { matchedPath, params, route, httpMiddleware, middleware, meta } =
236
+ matchedRoute
263
237
  const {
264
238
  singletonServices,
265
239
  userSession,
@@ -347,14 +321,19 @@ const executeRouteWithMiddleware = async (
347
321
  })
348
322
  }
349
323
 
350
- result = await runPikkuFunc(meta.pikkuFuncName, {
351
- getAllServices,
352
- session,
353
- data,
354
- permissions: route.permissions,
355
- coerceDataFromSchema: options.coerceDataFromSchema,
356
- tags: route.tags,
357
- })
324
+ result = await runPikkuFunc(
325
+ PikkuWiringTypes.http,
326
+ `${meta.method}:${meta.route}`,
327
+ meta.pikkuFuncName,
328
+ {
329
+ getAllServices,
330
+ session,
331
+ data,
332
+ permissions: route.permissions,
333
+ coerceDataFromSchema: options.coerceDataFromSchema,
334
+ tags: route.tags,
335
+ }
336
+ )
358
337
 
359
338
  // Respond with either a binary or JSON response based on configuration
360
339
  if (route.returnsJSON === false) {
@@ -375,7 +354,8 @@ const executeRouteWithMiddleware = async (
375
354
  await runMiddleware(
376
355
  { ...singletonServices, userSession },
377
356
  { http },
378
- combineMiddleware({
357
+ combineMiddleware(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, {
358
+ httpMiddleware,
379
359
  wiringMiddleware: middleware,
380
360
  wiringTags: route.tags,
381
361
  funcMiddleware: funcConfig?.middleware,
@@ -506,7 +486,8 @@ export const fetchData = async <In, Out>(
506
486
  ignoreMiddleware
507
487
  ? {
508
488
  ...matchedRoute,
509
- middleware: [],
489
+ middleware: undefined,
490
+ httpMiddleware: undefined,
510
491
  }
511
492
  : matchedRoute,
512
493
  http!,
@@ -208,7 +208,7 @@ export type HTTPWiringMeta = {
208
208
  tags?: string[]
209
209
  sse?: true
210
210
  }
211
- export type HTTPWiringsMeta = Array<HTTPWiringMeta>
211
+ export type HTTPWiringsMeta = Record<HTTPMethod, Record<string, HTTPWiringMeta>>
212
212
 
213
213
  export type HTTPFunctionsMeta = Array<{
214
214
  name: string
@@ -57,6 +57,9 @@ export class PikkuFetchHTTPRequest<In = unknown>
57
57
  * @returns An object containing the cookies.
58
58
  */
59
59
  public cookie(cookieName: string): string | null {
60
+ if (this.#cookies?.[cookieName]) {
61
+ return this.#cookies[cookieName]
62
+ }
60
63
  const cookieHeader = this.header('cookie')
61
64
  this.#cookies = cookieHeader ? parseCookie(cookieHeader) : {}
62
65
  return this.#cookies[cookieName] || null
@@ -0,0 +1,15 @@
1
+ import { CorePikkuMiddleware } from '../../../types/core.types.js'
2
+ import { HTTPMethod } from '../http.types.js'
3
+ import { PathToRegexRouter } from './path-to-regex.js'
4
+
5
+ export type MatchResult = {
6
+ route: string
7
+ params: Record<string, any>
8
+ middleware?: CorePikkuMiddleware[]
9
+ } | null
10
+
11
+ export interface Router {
12
+ match(method: HTTPMethod, path: string): MatchResult
13
+ }
14
+
15
+ export const httpRouter = new PathToRegexRouter()
@@ -0,0 +1,309 @@
1
+ import { test, describe, beforeEach } from 'node:test'
2
+ import * as assert from 'node:assert/strict'
3
+ import { PathToRegexRouter } from './path-to-regex.js'
4
+ import { resetPikkuState, pikkuState } from '../../../pikku-state.js'
5
+ import { HTTPMethod } from '../http.types.js'
6
+
7
+ describe('PathToRegexRouter', () => {
8
+ let router: PathToRegexRouter
9
+
10
+ // Helper to create mock route wiring
11
+ const mockRoute = (route: string) => ({
12
+ route,
13
+ method: 'get' as HTTPMethod,
14
+ func: (() => {}) as any,
15
+ })
16
+
17
+ beforeEach(() => {
18
+ resetPikkuState()
19
+ router = new PathToRegexRouter()
20
+ })
21
+
22
+ describe('Static Route Optimization', () => {
23
+ test('should correctly identify static routes', () => {
24
+ // Setup test routes
25
+ pikkuState(
26
+ 'http',
27
+ 'routes',
28
+ new Map([
29
+ [
30
+ 'get',
31
+ new Map([
32
+ ['/static', mockRoute('/static')],
33
+ ['/api/about', mockRoute('/api/about')],
34
+ ['/health', mockRoute('/health')],
35
+ ]),
36
+ ],
37
+ ])
38
+ )
39
+ pikkuState('http', 'middleware', new Map())
40
+ pikkuState('channel', 'channels', new Map())
41
+
42
+ router.initialize()
43
+
44
+ // Test static routes - should return exact matches with empty params
45
+ const staticResult = router.match('get', '/static')
46
+ assert.ok(staticResult, 'Static route should match')
47
+ assert.strictEqual(staticResult.route, '/static')
48
+ assert.deepStrictEqual(staticResult.params, {})
49
+ assert.strictEqual(staticResult.middleware?.length, 0)
50
+
51
+ const aboutResult = router.match('get', '/api/about')
52
+ assert.ok(aboutResult, 'Static route /api/about should match')
53
+ assert.strictEqual(aboutResult.route, '/api/about')
54
+ assert.deepStrictEqual(aboutResult.params, {})
55
+ })
56
+
57
+ test('should handle dynamic routes with parameters', () => {
58
+ // Setup test routes with parameters
59
+ pikkuState(
60
+ 'http',
61
+ 'routes',
62
+ new Map([
63
+ [
64
+ 'get',
65
+ new Map([
66
+ ['/api/users/:id', mockRoute('/api/users/:id')],
67
+ [
68
+ '/posts/:slug/comments/:commentId',
69
+ mockRoute('/posts/:slug/comments/:commentId'),
70
+ ],
71
+ ]),
72
+ ],
73
+ ])
74
+ )
75
+ pikkuState('http', 'middleware', new Map())
76
+ pikkuState('channel', 'channels', new Map())
77
+
78
+ router.initialize()
79
+
80
+ // Test dynamic routes - should return matches with extracted parameters
81
+ const userResult = router.match('get', '/api/users/123')
82
+ assert.ok(userResult, 'Dynamic route should match')
83
+ assert.strictEqual(userResult.route, '/api/users/:id')
84
+ assert.strictEqual(userResult.params.id, '123')
85
+
86
+ const commentResult = router.match(
87
+ 'get',
88
+ '/posts/hello-world/comments/456'
89
+ )
90
+ assert.ok(commentResult, 'Complex dynamic route should match')
91
+ assert.strictEqual(
92
+ commentResult.route,
93
+ '/posts/:slug/comments/:commentId'
94
+ )
95
+ assert.strictEqual(commentResult.params.slug, 'hello-world')
96
+ assert.strictEqual(commentResult.params.commentId, '456')
97
+ })
98
+
99
+ test('should handle multi-parameter routes', () => {
100
+ // Setup routes with multiple parameters
101
+ pikkuState(
102
+ 'http',
103
+ 'routes',
104
+ new Map([
105
+ [
106
+ 'get',
107
+ new Map([
108
+ [
109
+ '/api/:version/users/:userId',
110
+ mockRoute('/api/:version/users/:userId'),
111
+ ],
112
+ [
113
+ '/blog/:year/:month/:slug',
114
+ mockRoute('/blog/:year/:month/:slug'),
115
+ ],
116
+ ]),
117
+ ],
118
+ ])
119
+ )
120
+ pikkuState('http', 'middleware', new Map())
121
+ pikkuState('channel', 'channels', new Map())
122
+
123
+ router.initialize()
124
+
125
+ // Test multi-parameter routes
126
+ const apiResult = router.match('get', '/api/v2/users/456')
127
+ assert.ok(apiResult, 'Multi-parameter route should match')
128
+ assert.strictEqual(apiResult.route, '/api/:version/users/:userId')
129
+ assert.strictEqual(apiResult.params.version, 'v2')
130
+ assert.strictEqual(apiResult.params.userId, '456')
131
+
132
+ const blogResult = router.match('get', '/blog/2024/03/hello-world')
133
+ assert.ok(blogResult, 'Deep multi-parameter route should match')
134
+ assert.strictEqual(blogResult.route, '/blog/:year/:month/:slug')
135
+ assert.strictEqual(blogResult.params.year, '2024')
136
+ assert.strictEqual(blogResult.params.month, '03')
137
+ assert.strictEqual(blogResult.params.slug, 'hello-world')
138
+ })
139
+
140
+ test('should prioritize static routes over dynamic routes for performance', () => {
141
+ // Setup mixed routes where static could conflict with dynamic
142
+ pikkuState(
143
+ 'http',
144
+ 'routes',
145
+ new Map([
146
+ [
147
+ 'get',
148
+ new Map([
149
+ ['/api/users', mockRoute('/api/users')], // static
150
+ ['/api/:resource', mockRoute('/api/:resource')], // dynamic that could match above
151
+ ]),
152
+ ],
153
+ ])
154
+ )
155
+ pikkuState('http', 'middleware', new Map())
156
+ pikkuState('channel', 'channels', new Map())
157
+
158
+ router.initialize()
159
+
160
+ // Static route should be found first (O(1) lookup)
161
+ const result = router.match('get', '/api/users')
162
+ assert.ok(result, 'Route should match')
163
+ assert.strictEqual(result.route, '/api/users') // Should match static, not dynamic
164
+ assert.deepStrictEqual(result.params, {})
165
+ })
166
+ })
167
+
168
+ describe('Path Normalization', () => {
169
+ test('should normalize paths without leading slash', () => {
170
+ pikkuState(
171
+ 'http',
172
+ 'routes',
173
+ new Map([
174
+ [
175
+ 'get',
176
+ new Map([
177
+ ['test', mockRoute('test')], // route without leading slash
178
+ ['api/health', mockRoute('api/health')],
179
+ ]),
180
+ ],
181
+ ])
182
+ )
183
+ pikkuState('http', 'middleware', new Map())
184
+ pikkuState('channel', 'channels', new Map())
185
+
186
+ router.initialize()
187
+
188
+ // Should match with or without leading slash in request
189
+ const result1 = router.match('get', '/test')
190
+ assert.ok(result1, 'Should match /test')
191
+ assert.strictEqual(result1.route, 'test')
192
+
193
+ const result2 = router.match('get', 'test')
194
+ assert.ok(result2, 'Should match test')
195
+ assert.strictEqual(result2.route, 'test')
196
+
197
+ const result3 = router.match('get', '/api/health')
198
+ assert.ok(result3, 'Should match /api/health')
199
+ assert.strictEqual(result3.route, 'api/health')
200
+ })
201
+ })
202
+
203
+ describe('Channel Routes Integration', () => {
204
+ test('should handle channel routes as GET routes', () => {
205
+ pikkuState('http', 'routes', new Map())
206
+ pikkuState('http', 'middleware', new Map())
207
+ pikkuState(
208
+ 'channel',
209
+ 'channels',
210
+ new Map([
211
+ ['websocket-test', { name: 'websocket-test', route: '/ws/test' }],
212
+ ['chat', { name: 'chat', route: '/chat/:room' }],
213
+ ])
214
+ )
215
+
216
+ router.initialize()
217
+
218
+ // Channel routes should be accessible via GET method
219
+ const wsResult = router.match('get', '/ws/test')
220
+ assert.ok(wsResult, 'Static channel route should match')
221
+ assert.strictEqual(wsResult.route, '/ws/test')
222
+ assert.deepStrictEqual(wsResult.params, {})
223
+
224
+ const chatResult = router.match('get', '/chat/general')
225
+ assert.ok(chatResult, 'Dynamic channel route should match')
226
+ assert.strictEqual(chatResult.route, '/chat/:room')
227
+ assert.strictEqual(chatResult.params.room, 'general')
228
+ })
229
+ })
230
+
231
+ describe('Middleware Integration', () => {
232
+ test('should include global and route-specific middleware', () => {
233
+ const globalMiddleware = [async () => {}]
234
+ const routeMiddleware = [async () => {}]
235
+
236
+ pikkuState(
237
+ 'http',
238
+ 'routes',
239
+ new Map([['get', new Map([['/api/test', mockRoute('/api/test')]])]])
240
+ )
241
+ pikkuState(
242
+ 'http',
243
+ 'middleware',
244
+ new Map([
245
+ ['*', globalMiddleware],
246
+ ['/api/test', routeMiddleware],
247
+ ])
248
+ )
249
+ pikkuState('channel', 'channels', new Map())
250
+
251
+ router.initialize()
252
+
253
+ const result = router.match('get', '/api/test')
254
+ assert.ok(result, 'Route should match')
255
+ assert.strictEqual(result.middleware?.length, 2) // global + route-specific
256
+ assert.strictEqual(result.middleware?.[0], globalMiddleware[0])
257
+ assert.strictEqual(result.middleware?.[1], routeMiddleware[0])
258
+ })
259
+ })
260
+
261
+ describe('No Match Scenarios', () => {
262
+ test('should return null for non-existent routes', () => {
263
+ pikkuState(
264
+ 'http',
265
+ 'routes',
266
+ new Map([['get', new Map([['/existing', mockRoute('/existing')]])]])
267
+ )
268
+ pikkuState('http', 'middleware', new Map())
269
+ pikkuState('channel', 'channels', new Map())
270
+
271
+ router.initialize()
272
+
273
+ const result = router.match('get', '/non-existent')
274
+ assert.strictEqual(result, null)
275
+ })
276
+
277
+ test('should return null for unsupported HTTP methods', () => {
278
+ pikkuState(
279
+ 'http',
280
+ 'routes',
281
+ new Map([['get', new Map([['/test', mockRoute('/test')]])]])
282
+ )
283
+ pikkuState('http', 'middleware', new Map())
284
+ pikkuState('channel', 'channels', new Map())
285
+
286
+ router.initialize()
287
+
288
+ const result = router.match('post', '/test')
289
+ assert.strictEqual(result, null)
290
+ })
291
+ })
292
+
293
+ describe('Lazy Initialization', () => {
294
+ test('should initialize automatically on first match call', () => {
295
+ pikkuState(
296
+ 'http',
297
+ 'routes',
298
+ new Map([['get', new Map([['/test', mockRoute('/test')]])]])
299
+ )
300
+ pikkuState('http', 'middleware', new Map())
301
+ pikkuState('channel', 'channels', new Map())
302
+
303
+ // Don't call initialize manually
304
+ const result = router.match('get', '/test')
305
+ assert.ok(result, 'Should auto-initialize and match')
306
+ assert.strictEqual(result.route, '/test')
307
+ })
308
+ })
309
+ })