@timber-js/app 0.2.0-alpha.165 → 0.2.0-alpha.167

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 (113) hide show
  1. package/dist/_chunks/{actions-CSDD6x7U.js → actions-TSxpXLHJ.js} +3 -3
  2. package/dist/_chunks/{actions-CSDD6x7U.js.map → actions-TSxpXLHJ.js.map} +1 -1
  3. package/dist/_chunks/{cache-api-eb1gydM7.js → cache-api-DzpQQOEx.js} +53 -49
  4. package/dist/_chunks/{cache-api-eb1gydM7.js.map → cache-api-DzpQQOEx.js.map} +1 -1
  5. package/dist/_chunks/{cli-schema-sync-mGfRbjh2.js → cli-schema-sync-wX-i90Og.js} +6 -4
  6. package/dist/_chunks/cli-schema-sync-wX-i90Og.js.map +1 -0
  7. package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
  8. package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
  9. package/dist/_chunks/{define-CFmvb4Bt.js → define-COtkxMRT.js} +16 -28
  10. package/dist/_chunks/define-COtkxMRT.js.map +1 -0
  11. package/dist/_chunks/{define-Bssfp6ot.js → define-c4au4I9R.js} +2 -2
  12. package/dist/_chunks/{define-Bssfp6ot.js.map → define-c4au4I9R.js.map} +1 -1
  13. package/dist/_chunks/{logger-B_O6-mdJ.js → logger-t3uxAmbX.js} +3 -3
  14. package/dist/_chunks/logger-t3uxAmbX.js.map +1 -0
  15. package/dist/_chunks/{plugin-context-BnaiU_cF.js → plugin-context---kTF5v8.js} +2 -2
  16. package/dist/_chunks/{plugin-context-BnaiU_cF.js.map → plugin-context---kTF5v8.js.map} +1 -1
  17. package/dist/_chunks/{resolve-schema-3iUvBV5T.js → resolve-schema-Dz3fcFUo.js} +2 -2
  18. package/dist/_chunks/{resolve-schema-3iUvBV5T.js.map → resolve-schema-Dz3fcFUo.js.map} +1 -1
  19. package/dist/_chunks/{schema-bridge-BY3QLBL7.js → schema-bridge-DT_Tn0Xf.js} +2 -2
  20. package/dist/_chunks/{schema-bridge-BY3QLBL7.js.map → schema-bridge-DT_Tn0Xf.js.map} +1 -1
  21. package/dist/_chunks/{use-query-states-CbeQmext.js → use-query-states-DFvWd-EA.js} +65 -7
  22. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +1 -0
  23. package/dist/_chunks/{walkers-BL3MCMgO.js → walkers-Cfwvl-UC.js} +3 -3
  24. package/dist/_chunks/{walkers-BL3MCMgO.js.map → walkers-Cfwvl-UC.js.map} +1 -1
  25. package/dist/adapters/cloudflare-dev.js +1 -1
  26. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  27. package/dist/adapters/cloudflare.d.ts +12 -1
  28. package/dist/adapters/cloudflare.d.ts.map +1 -1
  29. package/dist/adapters/cloudflare.js +2 -461
  30. package/dist/adapters/nitro.js +1 -1
  31. package/dist/adapters/types.d.ts +2 -0
  32. package/dist/adapters/types.d.ts.map +1 -1
  33. package/dist/cache/cache-api.d.ts +33 -11
  34. package/dist/cache/cache-api.d.ts.map +1 -1
  35. package/dist/cache/index.js +1 -1
  36. package/dist/cli.js +2 -2
  37. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  38. package/dist/client/history.d.ts +10 -0
  39. package/dist/client/history.d.ts.map +1 -1
  40. package/dist/client/internal.js +139 -115
  41. package/dist/client/internal.js.map +1 -1
  42. package/dist/client/router.d.ts.map +1 -1
  43. package/dist/client/use-query-states.d.ts.map +1 -1
  44. package/dist/codec.js +1 -1
  45. package/dist/cookies/index.js +1 -1
  46. package/dist/dev-tools/logs.d.ts +15 -1
  47. package/dist/dev-tools/logs.d.ts.map +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +138 -45
  50. package/dist/index.js.map +1 -1
  51. package/dist/params/index.js +1 -1
  52. package/dist/plugins/adapter-build.d.ts.map +1 -1
  53. package/dist/plugins/cache.d.ts +8 -8
  54. package/dist/plugins/cache.d.ts.map +1 -1
  55. package/dist/routing/index.js +2 -2
  56. package/dist/schema-bridge.d.ts.map +1 -1
  57. package/dist/search-params/define.d.ts +35 -4
  58. package/dist/search-params/define.d.ts.map +1 -1
  59. package/dist/search-params/index.js +3 -7
  60. package/dist/search-params/index.js.map +1 -1
  61. package/dist/search-params/wrappers.d.ts +2 -2
  62. package/dist/search-params/wrappers.d.ts.map +1 -1
  63. package/dist/segment-params/index.js +1 -1
  64. package/dist/server/als-registry.d.ts +7 -0
  65. package/dist/server/als-registry.d.ts.map +1 -1
  66. package/dist/server/deny-boundary.d.ts +3 -1
  67. package/dist/server/deny-boundary.d.ts.map +1 -1
  68. package/dist/server/index.js +2 -2
  69. package/dist/server/internal.js +12 -7
  70. package/dist/server/internal.js.map +1 -1
  71. package/dist/server/route-element-builder.d.ts +9 -0
  72. package/dist/server/route-element-builder.d.ts.map +1 -1
  73. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  74. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  75. package/dist/server/rsc-entry/rsc-stream.d.ts +8 -0
  76. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  77. package/dist/server/stream-utils.d.ts.map +1 -1
  78. package/docs/api/32-api-cache.mdx +4 -4
  79. package/docs/api/33-api-search-params.mdx +13 -0
  80. package/docs/learn/00-introduction.mdx +1 -2
  81. package/docs/learn/09-caching.mdx +5 -5
  82. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  83. package/docs/more/50-ai-agent-instructions.mdx +5 -5
  84. package/package.json +10 -8
  85. package/src/adapters/cloudflare.ts +63 -25
  86. package/src/adapters/types.ts +2 -0
  87. package/src/cache/cache-api.ts +84 -84
  88. package/src/cli.ts +0 -0
  89. package/src/client/browser-entry/post-hydration.ts +16 -9
  90. package/src/client/history.ts +11 -0
  91. package/src/client/router.ts +212 -206
  92. package/src/client/use-query-states.ts +99 -7
  93. package/src/dev-tools/logs.ts +119 -34
  94. package/src/index.ts +14 -1
  95. package/src/plugins/adapter-build.ts +1 -0
  96. package/src/plugins/cache.ts +45 -30
  97. package/src/routing/scanner.ts +7 -0
  98. package/src/schema-bridge.ts +4 -3
  99. package/src/search-params/define.ts +57 -51
  100. package/src/search-params/wrappers.ts +17 -26
  101. package/src/server/als-registry.ts +7 -0
  102. package/src/server/deny-boundary.ts +6 -3
  103. package/src/server/route-element-builder.ts +49 -8
  104. package/src/server/rsc-entry/render-route.ts +3 -1
  105. package/src/server/rsc-entry/rsc-payload.ts +21 -4
  106. package/src/server/rsc-entry/rsc-stream.ts +8 -0
  107. package/src/server/stream-utils.ts +8 -6
  108. package/LICENSE +0 -8
  109. package/dist/_chunks/cli-schema-sync-mGfRbjh2.js.map +0 -1
  110. package/dist/_chunks/define-CFmvb4Bt.js.map +0 -1
  111. package/dist/_chunks/logger-B_O6-mdJ.js.map +0 -1
  112. package/dist/_chunks/use-query-states-CbeQmext.js.map +0 -1
  113. package/dist/adapters/cloudflare.js.map +0 -1
@@ -73,6 +73,15 @@ export interface RouteElementResult {
73
73
  * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
74
74
  */
75
75
  skippedSegments: string[];
76
+ /**
77
+ * Resolves when every deny-capable shell component (PageDenyBoundary and
78
+ * each TracedLayout with a deny page chain) has finished executing, OR
79
+ * immediately when any of them catches a DenySignal in-tree (the HTTP
80
+ * status is known at that point; inner components may never run).
81
+ * Undefined when the route has no deny-capable server components.
82
+ * See TIM-1045 (pages), TIM-1208 (layouts).
83
+ */
84
+ shellSettled?: Promise<void>;
76
85
  }
77
86
  /**
78
87
  * Build a React element tree from a matched route.
@@ -1 +1 @@
1
- {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AA4B9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC,YAAY,CAW/E;AAwCD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAID,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CA0gB7B"}
1
+ {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AA6B9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC,YAAY,CAW/E;AAwCD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAID,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CAyiB7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"render-route.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/render-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAW1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAMtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAa/D;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,mBAAmB,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,IAAI,EAAE,eAAe,EACrB,YAAY,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,QAAQ,CAAC,CA+MnB"}
1
+ {"version":3,"file":"render-route.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/render-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAW1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAMtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAa/D;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,mBAAmB,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,IAAI,EAAE,eAAe,EACrB,YAAY,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAiNnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC,QAAQ,CAAC,CAmKnB"}
1
+ {"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC,QAAQ,CAAC,CAoLnB"}
@@ -40,6 +40,14 @@ export interface RenderSignals {
40
40
  lastUnhandledError: unknown | null;
41
41
  /** Callback fired when a redirect or deny signal is captured in onError. */
42
42
  onSignal?: () => void;
43
+ /**
44
+ * Resolves when every deny-capable shell component (PageDenyBoundary +
45
+ * TracedLayouts with deny chains) has settled, OR immediately when any
46
+ * catches a DenySignal in-tree (status is known). Set by renderRoute
47
+ * after element-tree construction. Awaited by buildRscPayloadResponse
48
+ * before committing HTTP status. See TIM-1045, TIM-1208.
49
+ */
50
+ shellSettled?: Promise<void>;
43
51
  /** Callback fired when an unhandled error (non-signal) is captured. Used
44
52
  * to halt Flight data injection into the inline stream. */
45
53
  onUnhandledError?: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAe,MAAM,kBAAkB,CAAC;AAG3E,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAItB;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;IACtC,WAAW,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvD;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;gEAC4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;IAClD,OAAO,EAAE,aAAa,CAAC;IACvB,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,MAAM,mBAAmB,EAAE,CAAC;CAClD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CA6J1F"}
1
+ {"version":3,"file":"rsc-stream.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAe,MAAM,kBAAkB,CAAC;AAG3E,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAItB;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;IACtC,WAAW,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvD;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;gEAC4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;IAClD,OAAO,EAAE,aAAa,CAAC;IACvB,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,MAAM,mBAAmB,EAAE,CAAC;CAClD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CA6J1F"}
@@ -1 +1 @@
1
- {"version":3,"file":"stream-utils.d.ts","sourceRoot":"","sources":["../../src/server/stream-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,UAAU,GACnB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC,CAmK1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,SAAS,EAAE,MAAM,GAChB,cAAc,CAAC,UAAU,CAAC,CA0D5B"}
1
+ {"version":3,"file":"stream-utils.d.ts","sourceRoot":"","sources":["../../src/server/stream-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,UAAU,GACnB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC,CAmK1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,SAAS,EAAE,MAAM,GAChB,cAAc,CAAC,UAAU,CAAC,CA4D5B"}
@@ -8,7 +8,7 @@ slug: 'api-cache'
8
8
 
9
9
  Caching primitives and cache handler interface. Import from `@timber-js/app/cache`.
10
10
 
11
- ## `cache(fn, options)`
11
+ ## `cache.data(fn, options)`
12
12
 
13
13
  Wrap an async function with cross-request caching.
14
14
 
@@ -17,7 +17,7 @@ import { cache } from '@timber-js/app/cache';
17
17
 
18
18
  declare const db: { users: { findUnique(opts: { where: { id: string } }): Promise<{ id: string; name: string }> } };
19
19
 
20
- const getUser = cache(
20
+ const getUser = cache.data(
21
21
  async (userId: string) => {
22
22
  return db.users.findUnique({ where: { id: userId } });
23
23
  },
@@ -41,9 +41,9 @@ const getUser = cache(
41
41
 
42
42
  Default key: FNV-1a hash of function identity + deterministic JSON serialization of arguments.
43
43
 
44
- The function identity is derived at build time from the source text of the `cache(...)` call, so it is stable across restarts, instances, and deploys. With a persistent shared handler (Redis/KV), cached entries survive deploys as long as the call itself is unchanged — reordering calls or editing unrelated code preserves the cache, while editing the wrapped function or its options gives the call a fresh identity (old entries simply expire via TTL). Renaming or moving the file also resets identity for every call in it.
44
+ The function identity is derived at build time from the source text of the `cache.data(...)` call, so it is stable across restarts, instances, and deploys. With a persistent shared handler (Redis/KV), cached entries survive deploys as long as the call itself is unchanged — reordering calls or editing unrelated code preserves the cache, while editing the wrapped function or its options gives the call a fresh identity (old entries simply expire via TTL). Renaming or moving the file also resets identity for every call in it.
45
45
 
46
- Build-time identity covers calls the bundler can see statically: named imports (including aliases like `import { cache as c }`) and namespace imports (`import * as timber` + `timber.cache(fn, opts)`). Calls it can't trace — a local alias like `const c = cache`, or `cache` re-exported through your own wrapper module — fall back to a hash of the function's source text at runtime. The fallback is still deterministic across instances of the same build, but it resets across deploys, and byte-identical closures produced by a factory can collide — pass an explicit `key` in those cases (timber logs a warning when the fallback is used).
46
+ Build-time identity covers calls the bundler can see statically: named imports (including aliases like `import { cache as c }`) and namespace imports (`import * as timber` + `timber.cache.data(fn, opts)`). Calls it can't trace — a local alias like `const c = cache`, or `cache` re-exported through your own wrapper module — fall back to a hash of the function's source text at runtime. The fallback is still deterministic across instances of the same build, but it resets across deploys, and byte-identical closures produced by a factory can collide — pass an explicit `key` in those cases (timber logs a warning when the fallback is used).
47
47
 
48
48
  ### `cache.invalidate(options)`
49
49
 
@@ -23,6 +23,19 @@ export const searchParams = defineSearchParams({
23
23
  });
24
24
  ```
25
25
 
26
+ ### Implicit optionality
27
+
28
+ Search params are optional by nature — the URL might not contain them. A schema without `.default()` or `.optional()` (e.g. bare `z.string()`) is treated as implicitly optional: absent or invalid input parses to `undefined`, and the field's inferred type widens to `T | undefined`. Definitions never throw at module-eval time, and schema-backed parsing never throws at request time.
29
+
30
+ ```ts
31
+ const def = defineSearchParams({ name: z.string() });
32
+ def.parse(new URLSearchParams('')); // { name: undefined }
33
+ def.parse(new URLSearchParams('name=x')); // { name: 'x' }
34
+ // Inferred type: { name: string | undefined }
35
+ ```
36
+
37
+ Note: `z.coerce.*` schemas declare input `unknown`, so a coerce schema without `.default()` keeps its narrow type even though absent input yields `undefined` at runtime — add `.default()` to coerce schemas for accurate types.
38
+
26
39
  ### Returns: `SearchParamsDefinition<T>`
27
40
 
28
41
  | Method / Property | Description |
@@ -61,8 +61,7 @@ export const searchParams = defineSearchParams({
61
61
  const sp = searchParams.get();
62
62
 
63
63
  // or on the client
64
-
65
- const [sp, setSearchParams] = searchParams.useQueryState();
64
+ const [sp, setSearchParams] = searchParams.useQueryStates();
66
65
  ```
67
66
 
68
67
  ### Typed Routes
@@ -36,7 +36,7 @@ export const getUser = cache(async (id: string) => {
36
36
  });
37
37
 
38
38
  // Caches across requests for 60 seconds
39
- export const getPopularProducts = timberCache(
39
+ export const getPopularProducts = timberCache.data(
40
40
  async () => {
41
41
  return db.products.findPopular();
42
42
  },
@@ -44,7 +44,7 @@ export const getPopularProducts = timberCache(
44
44
  );
45
45
  ```
46
46
 
47
- ## `timber.cache()` Options
47
+ ## `cache.data()` Options
48
48
 
49
49
  ```ts
50
50
  import { cache } from '@timber-js/app/cache';
@@ -53,7 +53,7 @@ declare const db: {
53
53
  users: { findUnique(opts: { where: { id: string } }): Promise<{ id: string; name: string }> };
54
54
  };
55
55
 
56
- const getUser = cache(
56
+ const getUser = cache.data(
57
57
  async (userId: string) => {
58
58
  return db.users.findUnique({ where: { id: userId } });
59
59
  },
@@ -74,7 +74,7 @@ Without an explicit `key`, the cache key is derived from the call site plus a de
74
74
 
75
75
  ### Content-Derived Cache Identities
76
76
 
77
- The call-site identity is content-based — derived from the source code of the `cache()` call, not its position in the file. This means:
77
+ The call-site identity is content-based — derived from the source code of the `cache.data()` call, not its position in the file. This means:
78
78
 
79
79
  - **Unchanged code = cache survives deploys.** If you redeploy without changing a cached function, its entries remain valid on persistent handlers like Redis or KV.
80
80
  - **Changed code = automatic invalidation.** Editing the wrapped function or its options changes the cache identity, which invalidates old entries without manual cache busting.
@@ -86,7 +86,7 @@ Object key order doesn't matter — `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produc
86
86
  Arguments that can't be serialized faithfully — functions, symbols, or class instances whose data lives behind getters with no `toJSON()` — throw a `TypeError` instead of silently colliding. If you hit this, pass an explicit `key`:
87
87
 
88
88
  ```ts
89
- cache(fn, { ttl: 60, key: (session) => `events:${session.userId}` });
89
+ cache.data(fn, { ttl: 60, key: (session) => `events:${session.userId}` });
90
90
  ```
91
91
 
92
92
  ## Invalidation
@@ -80,14 +80,14 @@ export const searchParams = defineSearchParams({
80
80
 
81
81
  **Next.js:** Implicit fetch caching, `unstable_cache`, ISR with `revalidate`.
82
82
 
83
- **timber:** No implicit caching. No ISR. Explicit `timber.cache()` with TTL and tags:
83
+ **timber:** No implicit caching. No ISR. Explicit `cache.data()` with TTL and tags:
84
84
 
85
85
  ```ts
86
86
  import { cache } from '@timber-js/app/cache';
87
87
 
88
88
  declare const db: { products: { findPopular(): Promise<{ id: string; name: string }[]> } };
89
89
 
90
- const getProducts = cache(
90
+ const getProducts = cache.data(
91
91
  async () => db.products.findPopular(),
92
92
  { ttl: 300, tags: ['products'] }
93
93
  );
@@ -53,8 +53,8 @@ These are the things you will get wrong if you assume Next.js behavior.
53
53
  | Concept | Next.js | timber.js |
54
54
  | --- | --- | --- |
55
55
  | Default component type | Client components need `'use client'` | **All components are server components** by default. Only add `'use client'` when you need browser APIs or hooks. |
56
- | Fetch caching | `fetch()` is patched with implicit caching | `fetch()` is **never patched**. Use `cache()` from `@timber-js/app/cache` explicitly. |
57
- | ISR | `revalidate` option | **Does not exist.** Use `cache()` with TTL and tags. |
56
+ | Fetch caching | `fetch()` is patched with implicit caching | `fetch()` is **never patched**. Use `cache.data()` from `@timber-js/app/cache` explicitly. |
57
+ | ISR | `revalidate` option | **Does not exist.** Use `cache.data()` with TTL and tags. |
58
58
  | `loading.tsx` | Convention for auto-Suspense | **Does not exist.** Use `<Suspense>` explicitly. |
59
59
  | Middleware | Single global `middleware.ts` with matchers | Per-segment `middleware.ts` files + global `proxy.ts`. One-arg signature: `middleware(ctx)`. |
60
60
  | `notFound()` | `notFound()` from `next/navigation` | `deny(404)` from `@timber-js/app/server`. Sends a real HTTP 404. |
@@ -104,11 +104,11 @@ content-collections.ts # Collection definitions (optional)
104
104
 
105
105
  2. **Adding `'use client'` to everything** — components are server components by default. Only add `'use client'` when you need browser APIs, event handlers, or React hooks like `useState`/`useEffect`.
106
106
 
107
- 3. **Using `fetch()` and expecting caching** — timber never patches `fetch`. Wrap data-fetching functions in `cache()` from `@timber-js/app/cache` when you want caching.
107
+ 3. **Using `fetch()` and expecting caching** — timber never patches `fetch`. Wrap data-fetching functions in `cache.data()` from `@timber-js/app/cache` when you want caching.
108
108
 
109
109
  4. **Creating `loading.tsx` files** — this convention does not exist. Use `<Suspense>` with an explicit fallback where you want streaming.
110
110
 
111
- 5. **Using ISR patterns** (`revalidate`, `unstable_cache`) — ISR does not exist. Use `cache()` with `{ ttl: seconds, tags: ['tag'] }`.
111
+ 5. **Using ISR patterns** (`revalidate`, `unstable_cache`) — ISR does not exist. Use `cache.data()` with `{ ttl: seconds, tags: ['tag'] }`.
112
112
 
113
113
  6. **Calling `notFound()`** — use `deny(404)` from `@timber-js/app/server` instead. It sends a real HTTP 404 status code.
114
114
 
@@ -123,7 +123,7 @@ content-collections.ts # Collection definitions (optional)
123
123
  ```tsx
124
124
  import { cache } from '@timber-js/app/cache';
125
125
 
126
- const getProducts = cache(
126
+ const getProducts = cache.data(
127
127
  async () => db.products.findMany(),
128
128
  { ttl: 300, tags: ['products'] }
129
129
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.165",
3
+ "version": "0.2.0-alpha.167",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -150,6 +150,12 @@
150
150
  "publishConfig": {
151
151
  "access": "public"
152
152
  },
153
+ "scripts": {
154
+ "copy-docs": "node scripts/copy-docs.js",
155
+ "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
156
+ "typecheck": "tsgo --noEmit",
157
+ "prepublishOnly": "pnpm run build"
158
+ },
153
159
  "dependencies": {
154
160
  "@opentelemetry/api": "^1.9.1",
155
161
  "@opentelemetry/context-async-hooks": "^2.8.0",
@@ -157,7 +163,8 @@
157
163
  "cookie": "^1.1.1",
158
164
  "magic-string": "^0.30.21",
159
165
  "nitro": "3.0.260610-beta",
160
- "srvx": "^0.11.17"
166
+ "srvx": "^0.11.17",
167
+ "jsonc-parser": "^3.3.1"
161
168
  },
162
169
  "peerDependencies": {
163
170
  "@content-collections/core": "^0.14.0 || ^0.15.0",
@@ -195,10 +202,5 @@
195
202
  },
196
203
  "engines": {
197
204
  "node": ">=22.18.0"
198
- },
199
- "scripts": {
200
- "copy-docs": "node scripts/copy-docs.js",
201
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
202
- "typecheck": "tsgo --noEmit"
203
205
  }
204
- }
206
+ }
@@ -3,9 +3,10 @@
3
3
  // Primary deployment target. Generates a Workers-compatible entry point
4
4
  // and wrangler.jsonc configuration. See design/11-platform.md §"Cloudflare Workers".
5
5
 
6
- import { writeFile, readFile, cp, rm, readdir } from 'node:fs/promises';
6
+ import { writeFile, readFile, cp, rm, readdir, access } from 'node:fs/promises';
7
7
  import { execFile } from 'node:child_process';
8
8
  import { join, relative } from 'node:path';
9
+ import { parse as parseJsonc } from 'jsonc-parser';
9
10
  import { AsyncLocalStorage } from 'node:async_hooks';
10
11
  import type { TimberPlatformAdapter, TimberConfig } from './types';
11
12
  import { runSharedBuildSteps } from './build-output-helper.js';
@@ -158,6 +159,12 @@ export interface CloudflareBindings {
158
159
 
159
160
  /** Options for the Cloudflare Workers adapter. */
160
161
  export interface CloudflareAdapterOptions {
162
+ /**
163
+ * Worker name used in the generated wrangler.jsonc.
164
+ * @default 'timber-app'
165
+ */
166
+ name?: string;
167
+
161
168
  /**
162
169
  * Cloudflare compatibility date.
163
170
  * @default Current date in YYYY-MM-DD format at build time.
@@ -291,8 +298,10 @@ export function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatfo
291
298
  const workerEntry = generateWorkerEntry(outDir, outDir, hasManifestInit, hasWorkerHandlers);
292
299
  await writeFile(join(outDir, '_worker.js'), workerEntry);
293
300
 
294
- // Generate wrangler.jsonc
295
- const wranglerConfig = generateWranglerConfig(config, options);
301
+ // Generate wrangler.jsonc — merges user's wrangler config from disk if present
302
+ const root = config.root ?? process.cwd();
303
+ const userConfig = await readUserWranglerConfig(root);
304
+ const wranglerConfig = generateWranglerConfig(config, options, userConfig);
296
305
  await writeFile(join(outDir, 'wrangler.jsonc'), JSON.stringify(wranglerConfig, null, 2));
297
306
  },
298
307
 
@@ -682,22 +691,40 @@ function isPlainObject(val: unknown): val is Record<string, unknown> {
682
691
  return val !== null && typeof val === 'object' && !Array.isArray(val);
683
692
  }
684
693
 
694
+ const WRANGLER_CONFIG_FILES = ['wrangler.jsonc', 'wrangler.json'] as const;
695
+
696
+ /**
697
+ * Read the user's wrangler config from the project root, if one exists.
698
+ * Checks wrangler.jsonc first, then wrangler.json.
699
+ * @internal Exported for testing.
700
+ */
701
+ export async function readUserWranglerConfig(
702
+ root: string
703
+ ): Promise<Record<string, unknown> | null> {
704
+ for (const filename of WRANGLER_CONFIG_FILES) {
705
+ const filepath = join(root, filename);
706
+ try {
707
+ await access(filepath);
708
+ } catch {
709
+ continue;
710
+ }
711
+ const content = await readFile(filepath, 'utf-8');
712
+ return parseJsonc(content) as Record<string, unknown>;
713
+ }
714
+ return null;
715
+ }
716
+
685
717
  /** @internal Exported for testing. */
686
718
  export function generateWranglerConfig(
687
719
  config: TimberConfig,
688
- options: CloudflareAdapterOptions
720
+ options: CloudflareAdapterOptions,
721
+ userConfig?: Record<string, unknown> | null
689
722
  ): Record<string, unknown> {
690
- const compatDate = options.compatibilityDate ?? new Date().toISOString().slice(0, 10);
691
-
692
- const flags = options.compatibilityFlags ?? ['nodejs_compat'];
693
-
694
723
  const base: Record<string, unknown> = {
695
724
  name: 'timber-app',
696
725
  main: '_worker.js',
697
- compatibility_date: compatDate,
698
- compatibility_flags: flags,
699
- // The build output is already fully bundled by Vite — skip wrangler's
700
- // esbuild pass to avoid issues with top-level await and module format.
726
+ compatibility_date: new Date().toISOString().slice(0, 10),
727
+ compatibility_flags: ['nodejs_compat'],
701
728
  no_bundle: true,
702
729
  find_additional_modules: true,
703
730
  rules: [{ type: 'ESModule', globs: ['**/*.js'] }],
@@ -705,29 +732,40 @@ export function generateWranglerConfig(
705
732
  directory: './static',
706
733
  binding: 'ASSETS',
707
734
  },
708
- // Native trace destination — required for timber's pipeline spans
709
- // (emitted via tracing.enterSpan in the generated _worker.js) to appear
710
- // in the Cloudflare Observability dashboard. Gates traces only; does
711
- // not enable Workers Logs. Override via the `wrangler` escape hatch:
712
- // wrangler: { observability: { traces: { enabled: false } } }.
713
735
  observability: {
714
736
  traces: { enabled: true },
715
737
  },
716
738
  };
717
739
 
718
- // Layer 1: merge bindings-generated sections into base
740
+ // Layer 1: user's wrangler.jsonc / wrangler.json from disk
741
+ let merged: Record<string, unknown> = userConfig ? shallowDeepMerge(base, userConfig) : base;
742
+
743
+ // Layer 2: explicit adapter options override disk config
744
+ const explicit: Record<string, unknown> = {};
745
+ if (options.name != null) explicit.name = options.name;
746
+ if (options.compatibilityDate != null) explicit.compatibility_date = options.compatibilityDate;
747
+ if (options.compatibilityFlags != null) explicit.compatibility_flags = options.compatibilityFlags;
748
+ if (Object.keys(explicit).length > 0) {
749
+ merged = { ...merged, ...explicit };
750
+ }
751
+
752
+ // Layer 3: declarative bindings
719
753
  const bindingsConfig = generateBindingsConfig(options.bindings);
720
- const merged = { ...base, ...bindingsConfig };
754
+ merged = shallowDeepMerge(merged, bindingsConfig as Record<string, unknown>);
721
755
 
722
- // Layer 2: wrangler escape hatch with deep merge for nested plain objects.
723
- // A shallow spread would replace entire sections like durable_objects and
724
- // queues — e.g. adding migrations via wrangler would drop the generated
725
- // durable_objects.bindings. Deep merge preserves generated keys while
726
- // letting wrangler override on actual key-level conflicts.
756
+ // Layer 4: wrangler escape hatch — highest priority, deep merge.
727
757
  if (options.wrangler) {
728
- return shallowDeepMerge(merged, options.wrangler);
758
+ merged = shallowDeepMerge(merged, options.wrangler);
729
759
  }
730
760
 
761
+ // Generated config targets a single environment — env blocks can override
762
+ // main/assets per-environment and break the build output.
763
+ delete merged.env;
764
+
765
+ // These fields are required for the build output to work correctly.
766
+ merged.main = '_worker.js';
767
+ merged.find_additional_modules = true;
768
+
731
769
  return merged;
732
770
  }
733
771
 
@@ -9,6 +9,8 @@
9
9
  * A subset of the resolved timber.config.ts relevant to adapters.
10
10
  */
11
11
  export interface TimberConfig {
12
+ /** Absolute path to the project root (Vite root). */
13
+ root?: string;
12
14
  output: 'server' | 'static';
13
15
  clientJavascriptDisabled?: boolean;
14
16
  /**
@@ -6,102 +6,102 @@ import { recordInvalidation } from './invalidation-epoch';
6
6
  import { writeTombstonesForTag } from '../server/prebuilt/overlay.js';
7
7
  import { getCdnPurgeHandler } from '../cdn/purge-store.js';
8
8
 
9
+ const componentFallbackWarned = new WeakSet<object>();
10
+
9
11
  /**
10
- * Public caching API: `cache(fn, opts)`.
11
- *
12
- * Wraps an async function with cross-request caching. Uses the configured
13
- * cache handler (defaults to MemoryCacheHandler, overridable via timber.config.ts).
12
+ * Cache namespace — `cache.data`, `cache.component`, `cache.invalidate`.
14
13
  *
15
14
  * ```ts
16
15
  * import { cache } from '@timber-js/app/cache';
17
16
  *
18
- * const getUser = cache(
17
+ * const getUser = cache.data(
19
18
  * async (id: string) => db.users.findUnique({ where: { id } }),
20
19
  * { ttl: 60, tags: (id) => [`user:${id}`] }
21
20
  * );
22
21
  * ```
23
22
  */
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- export function cache<Fn extends (...args: any[]) => Promise<any>>(
26
- fn: Fn,
27
- opts: CacheOptions<Fn>,
28
- stableId?: string
29
- ): Fn {
30
- // Pass the getter, not a resolved handler: the wrapper re-resolves it on
31
- // every call, so module-scope wrappers created before the framework's boot
32
- // wiring calls setCacheHandler() still pick up the configured handler
33
- // instead of permanently binding the in-memory fallback (TIM-1029).
34
- return createCache(fn, opts, getCacheHandler, stableId);
35
- }
23
+ export const cache = {
24
+ /**
25
+ * Wrap an async function with cross-request caching.
26
+ *
27
+ * The configured cache handler (defaults to MemoryCacheHandler, overridable
28
+ * via timber.config.ts) is re-resolved on every call, so module-scope
29
+ * wrappers created before `setCacheHandler()` still pick up the configured
30
+ * handler (TIM-1029).
31
+ */
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ data<Fn extends (...args: any[]) => Promise<any>>(
34
+ fn: Fn,
35
+ opts: CacheOptions<Fn>,
36
+ stableId?: string
37
+ ): Fn {
38
+ return createCache(fn, opts, getCacheHandler, stableId);
39
+ },
36
40
 
37
- /**
38
- * Invalidate cache entries by tag or key.
39
- *
40
- * ```ts
41
- * cache.invalidate({ tag: 'products' });
42
- * cache.invalidate({ key: 'user:abc' });
43
- * ```
44
- */
45
- cache.invalidate = async function invalidate(opts: { key?: string; tag?: string }): Promise<void> {
46
- // Record before the handler delete so a cached fn in flight skips its
47
- // post-resolve set instead of resurrecting the invalidated value (TIM-1028).
48
- recordInvalidation(opts);
49
- await getCacheHandler().invalidate(opts);
50
- // Write tombstones for build-seed entries carrying the tag (design/45
51
- // §ISR mechanics). Runs AFTER handler.invalidate so the tombstone
52
- // overwrites the cleared overlay slot. This covers ALL invalidation
53
- // paths — actions, webhooks, route handlers — not just executeAction.
54
- if (opts.tag) {
55
- await writeTombstonesForTag(opts.tag);
56
- }
57
- // Trigger CDN purge if a handler is configured (design/25 §Layer 2).
58
- // Purge is best-effort: a CDN API failure or timeout must not block
59
- // cache invalidation. 10s ceiling prevents a hung CDN API from stalling
60
- // the entire revalidateTag/action flow.
61
- if (opts.tag) {
62
- const purgeHandler = getCdnPurgeHandler();
63
- if (purgeHandler) {
64
- try {
65
- await Promise.race([
66
- purgeHandler.purgeTags([opts.tag]),
67
- new Promise<void>((_, reject) =>
68
- setTimeout(() => reject(new Error('CDN purge timed out (10s)')), 10_000)
69
- ),
70
- ]);
71
- } catch (err) {
72
- console.error('[timber] CDN purge failed for tag:', err);
41
+ /**
42
+ * Invalidate cache entries by tag or key.
43
+ *
44
+ * ```ts
45
+ * cache.invalidate({ tag: 'products' });
46
+ * cache.invalidate({ key: 'user:abc' });
47
+ * ```
48
+ */
49
+ async invalidate(opts: { key?: string; tag?: string }): Promise<void> {
50
+ // Record before the handler delete so a cached fn in flight skips its
51
+ // post-resolve set instead of resurrecting the invalidated value (TIM-1028).
52
+ recordInvalidation(opts);
53
+ await getCacheHandler().invalidate(opts);
54
+ // Write tombstones for build-seed entries carrying the tag (design/45
55
+ // §ISR mechanics). Runs AFTER handler.invalidate so the tombstone
56
+ // overwrites the cleared overlay slot. This covers ALL invalidation
57
+ // paths — actions, webhooks, route handlers — not just executeAction.
58
+ if (opts.tag) {
59
+ await writeTombstonesForTag(opts.tag);
60
+ }
61
+ // Trigger CDN purge if a handler is configured (design/25 §Layer 2).
62
+ // Purge is best-effort: a CDN API failure or timeout must not block
63
+ // cache invalidation. 10s ceiling prevents a hung CDN API from stalling
64
+ // the entire revalidateTag/action flow.
65
+ if (opts.tag) {
66
+ const purgeHandler = getCdnPurgeHandler();
67
+ if (purgeHandler) {
68
+ try {
69
+ await Promise.race([
70
+ purgeHandler.purgeTags([opts.tag]),
71
+ new Promise<void>((_, reject) =>
72
+ setTimeout(() => reject(new Error('CDN purge timed out (10s)')), 10_000)
73
+ ),
74
+ ]);
75
+ } catch (err) {
76
+ console.error('[timber] CDN purge failed for tag:', err);
77
+ }
73
78
  }
74
79
  }
75
- }
76
- };
77
-
78
- const componentFallbackWarned = new WeakSet<object>();
80
+ },
79
81
 
80
- /**
81
- * Runtime fallback for `cache.component(...)` callsites the timber-prebuilt
82
- * transform does not rewrite — nested/computed/argument-position forms (see
83
- * plugins/prebuilt.ts). Only module-scope `const X = cache.component(...)`
84
- * declarations are transformed and eligible for prebuilding; everything else
85
- * lands here, warns once per component, and renders dynamically. Same
86
- * safety-net pattern as the data cache's runtime fnId fallback (TIM-1054):
87
- * an untraceable callsite degrades in performance, never in correctness.
88
- *
89
- * The full public surface (types, docs, `timber` namespace) is TIM-1121;
90
- * design/45-cache-lifetimes.md specifies the options.
91
- */
92
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
- cache.component = function component<C extends (props: any) => unknown>(
94
- componentFn: C,
95
- _options?: PrebuiltComponentOptions
96
- ): C {
97
- if (!componentFallbackWarned.has(componentFn)) {
98
- componentFallbackWarned.add(componentFn);
99
- const name = componentFn.name || 'anonymous component';
100
- console.warn(
101
- `[timber] cache.component: "${name}" was not transformed at build time — ` +
102
- `rendering dynamically. Only module-scope declarations ` +
103
- `(const X = cache.component(...)) are prebuilt.`
104
- );
105
- }
106
- return componentFn;
82
+ /**
83
+ * Runtime fallback for `cache.component(...)` callsites the timber-prebuilt
84
+ * transform does not rewrite — nested/computed/argument-position forms (see
85
+ * plugins/prebuilt.ts). Only module-scope `const X = cache.component(...)`
86
+ * declarations are transformed and eligible for prebuilding; everything else
87
+ * lands here, warns once per component, and renders dynamically. Same
88
+ * safety-net pattern as the data cache's runtime fnId fallback (TIM-1054):
89
+ * an untraceable callsite degrades in performance, never in correctness.
90
+ */
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ component<C extends (props: any) => unknown>(
93
+ componentFn: C,
94
+ _options?: PrebuiltComponentOptions
95
+ ): C {
96
+ if (!componentFallbackWarned.has(componentFn)) {
97
+ componentFallbackWarned.add(componentFn);
98
+ const name = componentFn.name || 'anonymous component';
99
+ console.warn(
100
+ `[timber] cache.component: "${name}" was not transformed at build time — ` +
101
+ `rendering dynamically. Only module-scope declarations ` +
102
+ `(const X = cache.component(...)) are prebuilt.`
103
+ );
104
+ }
105
+ return componentFn;
106
+ },
107
107
  };
package/src/cli.ts CHANGED
File without changes