@orkestrel/router 0.0.1 → 0.0.2

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.
@@ -1,3 +1,5 @@
1
+ import { Emitter } from "@orkestrel/emitter";
2
+ import { isFunction, isString } from "@orkestrel/contract";
1
3
  //#region src/core/constants.ts
2
4
  /**
3
5
  * The complete set of HTTP methods a {@link import('./types.js').Dispatcher}
@@ -410,189 +412,45 @@ function joinPaths(prefix, path) {
410
412
  if (path === "") return prefix;
411
413
  return `${prefix.endsWith("/") ? prefix.slice(0, -1) : prefix}${path.startsWith("/") ? path : `/${path}`}`;
412
414
  }
413
- //#endregion
414
- //#region node_modules/@orkestrel/emitter/dist/src/core/index.js
415
415
  /**
416
- * Extract the own enumerable keys of a mapped object, typed as its key union.
416
+ * Identity pass-through for a {@link RouteInput} that pins its `Path` generic
417
+ * to the LITERAL registration-site string, so `context.params` types
418
+ * correctly through {@link PathParams} without an explicit type argument.
417
419
  *
418
420
  * @remarks
419
- * `Object.keys` widens its result to `string[]`, which breaks the key↔value
420
- * correlation a mapped type (like `EmitterHooks<TMap>`) otherwise guarantees.
421
- * A `for…in` push into a `keyof`-typed array narrows the result back,
422
- * type-safely and with no assertion.
423
- *
424
- * @typeParam T - The object shape whose keys are extracted.
425
- * @param object - The object to read keys from.
426
- * @returns The object's own enumerable keys, typed as `(keyof T)[]`.
427
- *
428
- * @example
429
- * ```ts
430
- * import { extractKeys } from '@src/core'
431
- *
432
- * const hooks = { tick: () => {}, done: () => {} }
433
- * extractKeys(hooks) // ['tick', 'done']
434
- * extractKeys({}) // []
435
- * ```
436
- */
437
- function extractKeys(object) {
438
- const collected = [];
439
- for (const key in object) collected.push(key);
440
- return collected;
441
- }
442
- Object.freeze([
443
- "null",
444
- "boolean",
445
- "object",
446
- "array",
447
- "number",
448
- "integer",
449
- "string"
450
- ]);
451
- /** Determine whether a value is callable. */
452
- function isFunction$1(value) {
453
- return typeof value === "function";
454
- }
455
- /**
456
- * A typed synchronous event emitter — the foundational observable primitive of
457
- * the codebase (AGENTS §13). Stateful entities OWN one as a `#emitter` field and
458
- * expose it through `readonly emitter`; they never inherit from it.
459
- *
460
- * @typeParam TMap - The event map: each event name to the argument tuple its
461
- * listeners receive.
462
- *
463
- * @remarks
464
- * - **Synchronous.** `emit` invokes listeners in registration order, in the
465
- * current tick.
466
- * - **Listener isolation.** A throwing listener never stops its siblings: every
467
- * listener runs, and a throw is routed to the `error` handler
468
- * ({@link EmitterOptions.error}) — never rethrown. Every throwing listener
469
- * surfaces (not just the first), and with no `error` handler a throw is swallowed
470
- * silently. The `error` handler runs inside its own try/catch, so a throwing
471
- * error-handler is swallowed too (anti-recursion — it cannot escape or re-enter).
472
- * - **Per-event storage.** Listeners live in a per-event `Set`, so every public
473
- * method is precisely typed with no assertions.
474
- * - **Destroyed → no-op.** After `destroy()`, `on` / `once` / `emit` do nothing
475
- * and `destroyed` is `true`.
421
+ * A bare object literal handed straight to {@link import('./types.js').DispatcherInterface}'s
422
+ * `add` already infers `Path` as a literal at that call site — but the moment
423
+ * the object is built through an intermediate binding (a local `const route =
424
+ * { method, path, handler }`) TypeScript widens `path` to `string` unless the
425
+ * binding's own type is pinned. Wrapping the literal in `route(...)` supplies
426
+ * that pin: its `const Path extends string` type parameter infers the NARROW
427
+ * literal from the call, and the function returns its input completely
428
+ * unchanged (same reference, no cloning, no validation) this is a
429
+ * compile-time typing aid only, not a construction step (contrast
430
+ * {@link import('./factories.js')} `create*` entity factories). A
431
+ * heterogeneous `RouteInput[]` built from several `route(...)` calls still
432
+ * widens each element's `Path` to `string` once collected into one array
433
+ * (§14) — the realistic ceiling this helper raises is PER-CALL typing at the
434
+ * registration site, not a stored, still-literal-typed record.
435
+ *
436
+ * @typeParam Path - The route path pattern literal (drives `context.params`
437
+ * via {@link PathParams})
438
+ * @typeParam TState - The consumer's opaque per-request state type
439
+ * @param input - The {@link RouteInput} to pass through unchanged
440
+ * @returns `input`, unchanged (same reference)
476
441
  *
477
442
  * @example
478
443
  * ```ts
479
- * type CounterEventMap = {
480
- * tick: readonly [count: number]
481
- * done: readonly []
482
- * }
483
- *
484
- * const emitter = new Emitter<CounterEventMap>({
485
- * on: { done: () => stop() },
486
- * error: (error, event) => log(`listener for ${event} threw`, error),
444
+ * const input = route({
445
+ * method: 'GET',
446
+ * path: '/users/:id',
447
+ * handler: (_request, context) => new Response(context.params.id), // typed string
487
448
  * })
488
- * emitter.on('tick', (count) => render(count))
489
- * emitter.emit('tick', 1)
449
+ * dispatcher.add(input)
490
450
  * ```
491
451
  */
492
- var Emitter = class {
493
- #destroyed = false;
494
- #listeners = {};
495
- #wrappers = {};
496
- #error;
497
- constructor(options) {
498
- const error = options?.error;
499
- this.#error = isFunction$1(error) ? error : void 0;
500
- const hooks = options?.on;
501
- if (hooks !== void 0) this.#wire(hooks);
502
- }
503
- get destroyed() {
504
- return this.#destroyed;
505
- }
506
- on(event, handler) {
507
- if (this.#destroyed) return;
508
- (this.#listeners[event] ??= /* @__PURE__ */ new Set()).add(handler);
509
- }
510
- once(event, handler) {
511
- if (this.#destroyed) return;
512
- const pending = this.#wrappers[event] ??= /* @__PURE__ */ new Map();
513
- const wrapper = (...args) => {
514
- this.#listeners[event]?.delete(wrapper);
515
- const wrappers = pending.get(handler);
516
- wrappers?.delete(wrapper);
517
- if (wrappers !== void 0 && wrappers.size === 0) pending.delete(handler);
518
- handler(...args);
519
- };
520
- const wrappers = pending.get(handler) ?? /* @__PURE__ */ new Set();
521
- wrappers.add(wrapper);
522
- pending.set(handler, wrappers);
523
- this.on(event, wrapper);
524
- }
525
- off(event, handler) {
526
- const listeners = this.#listeners[event];
527
- const wrappers = this.#wrappers[event];
528
- const pending = wrappers?.get(handler);
529
- if (pending !== void 0) {
530
- for (const wrapper of pending) listeners?.delete(wrapper);
531
- wrappers?.delete(handler);
532
- }
533
- listeners?.delete(handler);
534
- }
535
- emit(event, ...args) {
536
- if (this.#destroyed) return;
537
- const listeners = this.#listeners[event];
538
- if (listeners === void 0) return;
539
- for (const handler of [...listeners]) try {
540
- handler(...args);
541
- } catch (error) {
542
- this.#surface(error, event);
543
- }
544
- }
545
- count(event) {
546
- if (event !== void 0) return this.#listeners[event]?.size ?? 0;
547
- let total = 0;
548
- for (const set of Object.values(this.#listeners)) total += set?.size ?? 0;
549
- return total;
550
- }
551
- clear(event) {
552
- if (event !== void 0) {
553
- delete this.#listeners[event];
554
- delete this.#wrappers[event];
555
- return;
556
- }
557
- this.#listeners = {};
558
- this.#wrappers = {};
559
- }
560
- destroy() {
561
- this.#listeners = {};
562
- this.#wrappers = {};
563
- this.#error = void 0;
564
- this.#destroyed = true;
565
- }
566
- #surface(error, event) {
567
- const handler = this.#error;
568
- if (handler === void 0) return;
569
- try {
570
- handler(error, String(event));
571
- } catch {}
572
- }
573
- #wire(hooks) {
574
- for (const event of extractKeys(hooks)) {
575
- const handler = hooks[event];
576
- if (isFunction$1(handler)) this.on(event, handler);
577
- }
578
- }
579
- };
580
- Object.freeze([
581
- "null",
582
- "boolean",
583
- "object",
584
- "array",
585
- "number",
586
- "integer",
587
- "string"
588
- ]);
589
- /** Determine whether a value is a string. */
590
- function isString(value) {
591
- return typeof value === "string";
592
- }
593
- /** Determine whether a value is callable. */
594
- function isFunction(value) {
595
- return typeof value === "function";
452
+ function route(input) {
453
+ return input;
596
454
  }
597
455
  //#endregion
598
456
  //#region src/core/Group.ts
@@ -1009,6 +867,6 @@ function createDispatcher(options) {
1009
867
  return new Dispatcher(options);
1010
868
  }
1011
869
  //#endregion
1012
- export { DispatchGroup, Dispatcher, Group, METHODS, Router, TIER_LITERAL, TIER_PARAM, TIER_WILDCARD, canonicalizePath, classifySegment, compareSpecificity, compilePath, computeSpecificity, createDispatcher, createRouter, decodeParam, escapeRegExp, joinPaths, matchPath, parseMethod };
870
+ export { DispatchGroup, Dispatcher, Group, METHODS, Router, TIER_LITERAL, TIER_PARAM, TIER_WILDCARD, canonicalizePath, classifySegment, compareSpecificity, compilePath, computeSpecificity, createDispatcher, createRouter, decodeParam, escapeRegExp, joinPaths, matchPath, parseMethod, route };
1013
871
 
1014
872
  //# sourceMappingURL=index.js.map