gemi 0.56.0 → 0.58.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.
Files changed (257) hide show
  1. package/dist/app/index.js +1 -1
  2. package/dist/broadcasting/index.js +1 -1
  3. package/dist/bun/plugin.js +1 -1
  4. package/dist/bun/preload.js +1 -1
  5. package/dist/{chunk-jdj7k3r9.js → chunk-0a2xgcj3.js} +2 -2
  6. package/dist/{chunk-jdj7k3r9.js.map → chunk-0a2xgcj3.js.map} +1 -1
  7. package/dist/{chunk-3e88tyee.js → chunk-3337e5g0.js} +2 -2
  8. package/dist/{chunk-3e88tyee.js.map → chunk-3337e5g0.js.map} +1 -1
  9. package/dist/chunk-3aa287k7.js +6 -0
  10. package/dist/{chunk-cv9w5cmb.js.map → chunk-3aa287k7.js.map} +2 -2
  11. package/dist/{chunk-01am9k5v.js → chunk-3g5bjvdf.js} +2 -2
  12. package/dist/{chunk-01am9k5v.js.map → chunk-3g5bjvdf.js.map} +1 -1
  13. package/dist/{chunk-w9k9s4wh.js → chunk-3xadx444.js} +2 -2
  14. package/dist/{chunk-w9k9s4wh.js.map → chunk-3xadx444.js.map} +1 -1
  15. package/dist/{chunk-npg72mez.js → chunk-3zxwscmf.js} +2 -2
  16. package/dist/{chunk-kgmr9qxx.js.map → chunk-3zxwscmf.js.map} +1 -1
  17. package/dist/{chunk-64s1pzz1.js → chunk-437085pe.js} +2 -2
  18. package/dist/{chunk-64s1pzz1.js.map → chunk-437085pe.js.map} +1 -1
  19. package/dist/{chunk-eqrd31ye.js → chunk-4yt5x8s2.js} +2 -2
  20. package/dist/{chunk-eqrd31ye.js.map → chunk-4yt5x8s2.js.map} +1 -1
  21. package/dist/{chunk-9r0sb4zn.js → chunk-5athahgr.js} +2 -2
  22. package/dist/{chunk-9r0sb4zn.js.map → chunk-5athahgr.js.map} +1 -1
  23. package/dist/{chunk-g30q4n5y.js → chunk-5n2rvfh3.js} +2 -2
  24. package/dist/{chunk-g30q4n5y.js.map → chunk-5n2rvfh3.js.map} +1 -1
  25. package/dist/{chunk-1zfsgffv.js → chunk-6235kb30.js} +3 -3
  26. package/dist/{chunk-1zfsgffv.js.map → chunk-6235kb30.js.map} +1 -1
  27. package/dist/{chunk-javjeayw.js → chunk-7b0x860b.js} +2 -2
  28. package/dist/{chunk-javjeayw.js.map → chunk-7b0x860b.js.map} +1 -1
  29. package/dist/{chunk-4qwwy968.js → chunk-7ef5n8k2.js} +2 -2
  30. package/dist/{chunk-4qwwy968.js.map → chunk-7ef5n8k2.js.map} +1 -1
  31. package/dist/{chunk-q0waxxz5.js → chunk-7j6wbv12.js} +2 -2
  32. package/dist/{chunk-q0waxxz5.js.map → chunk-7j6wbv12.js.map} +1 -1
  33. package/dist/chunk-86jebsm4.js +9 -0
  34. package/dist/{chunk-kry5vwam.js.map → chunk-86jebsm4.js.map} +3 -3
  35. package/dist/chunk-87qab82w.js +5 -0
  36. package/dist/chunk-87qab82w.js.map +37 -0
  37. package/dist/{chunk-36pg61vt.js → chunk-8gew8b9a.js} +2 -2
  38. package/dist/{chunk-36pg61vt.js.map → chunk-8gew8b9a.js.map} +1 -1
  39. package/dist/{chunk-v6v6sem5.js → chunk-9m2tbf3n.js} +2 -2
  40. package/dist/{chunk-v6v6sem5.js.map → chunk-9m2tbf3n.js.map} +1 -1
  41. package/dist/{chunk-gzdf2025.js → chunk-a2sgjpvq.js} +2 -2
  42. package/dist/{chunk-gzdf2025.js.map → chunk-a2sgjpvq.js.map} +1 -1
  43. package/dist/chunk-b35e128b.js +5 -0
  44. package/dist/{chunk-b50zmz3t.js.map → chunk-b35e128b.js.map} +1 -1
  45. package/dist/{chunk-ct274qts.js → chunk-cyaz97p5.js} +2 -2
  46. package/dist/{chunk-ct274qts.js.map → chunk-cyaz97p5.js.map} +1 -1
  47. package/dist/{chunk-v06qcyj5.js → chunk-d125j8t0.js} +3 -3
  48. package/dist/{chunk-v06qcyj5.js.map → chunk-d125j8t0.js.map} +1 -1
  49. package/dist/{chunk-hs5v3eqj.js → chunk-dgsgjg53.js} +2 -2
  50. package/dist/{chunk-hs5v3eqj.js.map → chunk-dgsgjg53.js.map} +1 -1
  51. package/dist/{chunk-xjy5apyr.js → chunk-eejmhtnc.js} +2 -2
  52. package/dist/{chunk-xjy5apyr.js.map → chunk-eejmhtnc.js.map} +1 -1
  53. package/dist/{chunk-3y75q5a2.js → chunk-fjm4y8bn.js} +2 -2
  54. package/dist/{chunk-3y75q5a2.js.map → chunk-fjm4y8bn.js.map} +1 -1
  55. package/dist/{chunk-9c89q2mz.js → chunk-grdahng8.js} +2 -2
  56. package/dist/{chunk-9c89q2mz.js.map → chunk-grdahng8.js.map} +1 -1
  57. package/dist/{chunk-699z6d8y.js → chunk-gw6agevz.js} +3 -3
  58. package/dist/{chunk-699z6d8y.js.map → chunk-gw6agevz.js.map} +1 -1
  59. package/dist/{chunk-62ke19q4.js → chunk-hppagzz4.js} +4 -4
  60. package/dist/{chunk-62ke19q4.js.map → chunk-hppagzz4.js.map} +1 -1
  61. package/dist/chunk-hwa5sqw5.js +19 -0
  62. package/dist/{chunk-tmnhkphv.js.map → chunk-hwa5sqw5.js.map} +12 -6
  63. package/dist/chunk-hxf1re93.js +4 -0
  64. package/dist/{chunk-vkngcrzq.js.map → chunk-hxf1re93.js.map} +6 -5
  65. package/dist/{chunk-d36dfqxw.js → chunk-j0c6ytkj.js} +3 -3
  66. package/dist/{chunk-d36dfqxw.js.map → chunk-j0c6ytkj.js.map} +1 -1
  67. package/dist/{chunk-kgmr9qxx.js → chunk-jhkjz9jr.js} +2 -2
  68. package/dist/{chunk-npg72mez.js.map → chunk-jhkjz9jr.js.map} +1 -1
  69. package/dist/{chunk-62723jyy.js → chunk-k0fvsyeh.js} +1 -1
  70. package/dist/{chunk-31kcf7dq.js → chunk-keehyx51.js} +2 -2
  71. package/dist/{chunk-31kcf7dq.js.map → chunk-keehyx51.js.map} +1 -1
  72. package/dist/{chunk-enhkf60v.js → chunk-m3xy5xyf.js} +2 -2
  73. package/dist/{chunk-enhkf60v.js.map → chunk-m3xy5xyf.js.map} +1 -1
  74. package/dist/{chunk-c75mymmq.js → chunk-mkfpnymy.js} +1 -1
  75. package/dist/{chunk-rgb69nh1.js → chunk-mwpdp09e.js} +2 -2
  76. package/dist/{chunk-rgb69nh1.js.map → chunk-mwpdp09e.js.map} +1 -1
  77. package/dist/chunk-pmhd6zfc.js +37 -0
  78. package/dist/chunk-pmhd6zfc.js.map +20 -0
  79. package/dist/chunk-qb5mv6pj.js +5 -0
  80. package/dist/chunk-qb5mv6pj.js.map +12 -0
  81. package/dist/{chunk-1pwwrpa3.js → chunk-qgxr0g36.js} +2 -2
  82. package/dist/{chunk-1pwwrpa3.js.map → chunk-qgxr0g36.js.map} +1 -1
  83. package/dist/{chunk-4yafsffx.js → chunk-sy7jbdeb.js} +2 -2
  84. package/dist/{chunk-4yafsffx.js.map → chunk-sy7jbdeb.js.map} +1 -1
  85. package/dist/{chunk-gasdfwva.js → chunk-szss069z.js} +2 -2
  86. package/dist/{chunk-gasdfwva.js.map → chunk-szss069z.js.map} +1 -1
  87. package/dist/{chunk-m0ggfy1m.js → chunk-tss5svjr.js} +2 -2
  88. package/dist/{chunk-m0ggfy1m.js.map → chunk-tss5svjr.js.map} +1 -1
  89. package/dist/{chunk-dzzmqv0j.js → chunk-vr90r27j.js} +2 -2
  90. package/dist/{chunk-dzzmqv0j.js.map → chunk-vr90r27j.js.map} +1 -1
  91. package/dist/{chunk-pvdbrt4z.js → chunk-w62m5f0n.js} +3 -3
  92. package/dist/{chunk-pvdbrt4z.js.map → chunk-w62m5f0n.js.map} +1 -1
  93. package/dist/{chunk-cn2r5jfj.js → chunk-w7rf99w6.js} +2 -2
  94. package/dist/{chunk-cn2r5jfj.js.map → chunk-w7rf99w6.js.map} +1 -1
  95. package/dist/{chunk-tja0c815.js → chunk-wbrj0gya.js} +2 -2
  96. package/dist/{chunk-tja0c815.js.map → chunk-wbrj0gya.js.map} +1 -1
  97. package/dist/{chunk-rsdg619q.js → chunk-xdv1b8mr.js} +2 -2
  98. package/dist/{chunk-rsdg619q.js.map → chunk-xdv1b8mr.js.map} +1 -1
  99. package/dist/{chunk-h3mwgbg7.js → chunk-xey9cbap.js} +2 -2
  100. package/dist/{chunk-h3mwgbg7.js.map → chunk-xey9cbap.js.map} +1 -1
  101. package/dist/{chunk-wgpa04jb.js → chunk-xzk827r3.js} +2 -2
  102. package/dist/{chunk-wgpa04jb.js.map → chunk-xzk827r3.js.map} +1 -1
  103. package/dist/{chunk-x14sk95v.js → chunk-y6a8r2bn.js} +3 -3
  104. package/dist/{chunk-x14sk95v.js.map → chunk-y6a8r2bn.js.map} +1 -1
  105. package/dist/{chunk-33wjsw4r.js → chunk-yed5whgs.js} +3 -3
  106. package/dist/{chunk-33wjsw4r.js.map → chunk-yed5whgs.js.map} +1 -1
  107. package/dist/{chunk-02gdzs5t.js → chunk-yf7vz71n.js} +1 -1
  108. package/dist/{chunk-y9fp58bg.js → chunk-yjzs247s.js} +2 -2
  109. package/dist/{chunk-y9fp58bg.js.map → chunk-yjzs247s.js.map} +1 -1
  110. package/dist/{chunk-kgg1eqne.js → chunk-yy0eb9wn.js} +2 -2
  111. package/dist/{chunk-kgg1eqne.js.map → chunk-yy0eb9wn.js.map} +1 -1
  112. package/dist/{chunk-4xx78ba9.js → chunk-zbxgbr12.js} +2 -2
  113. package/dist/{chunk-4xx78ba9.js.map → chunk-zbxgbr12.js.map} +1 -1
  114. package/dist/{chunk-8k1zqrvh.js → chunk-zh2egcyb.js} +2 -2
  115. package/dist/{chunk-8k1zqrvh.js.map → chunk-zh2egcyb.js.map} +1 -1
  116. package/dist/chunks/ThemeProvider-li1J_igh.js.map +1 -1
  117. package/dist/client/ClientRouter.d.ts.map +1 -1
  118. package/dist/client/ProgressManager.d.ts +1 -1
  119. package/dist/client/RouteStateContext.d.ts +9 -0
  120. package/dist/client/RouteStateContext.d.ts.map +1 -1
  121. package/dist/client/ServerDataProvider.d.ts +7 -0
  122. package/dist/client/ServerDataProvider.d.ts.map +1 -1
  123. package/dist/client/index.d.ts +2 -1
  124. package/dist/client/index.d.ts.map +1 -1
  125. package/dist/client/index.js +68 -10
  126. package/dist/client/index.js.map +1 -1
  127. package/dist/client/rpc.d.ts +42 -0
  128. package/dist/client/rpc.d.ts.map +1 -1
  129. package/dist/client/useFeature.d.ts +40 -0
  130. package/dist/client/useFeature.d.ts.map +1 -0
  131. package/dist/config/index.js +1 -1
  132. package/dist/console/run.js +2 -2
  133. package/dist/console/run.js.map +1 -1
  134. package/dist/container/index.js +2 -2
  135. package/dist/container/index.js.map +1 -1
  136. package/dist/database/index.js +2 -2
  137. package/dist/database/index.js.map +1 -1
  138. package/dist/email/index.js +2 -2
  139. package/dist/email/index.js.map +1 -1
  140. package/dist/facades/Features.d.ts +57 -0
  141. package/dist/facades/Features.d.ts.map +1 -0
  142. package/dist/facades/index.d.ts +1 -0
  143. package/dist/facades/index.d.ts.map +1 -1
  144. package/dist/facades/index.js +2 -2
  145. package/dist/facades/index.js.map +1 -1
  146. package/dist/foundation/index.js +2 -2
  147. package/dist/foundation/index.js.map +1 -1
  148. package/dist/gemi.d.ts +13 -5
  149. package/dist/http/ApiRouter.d.ts +3 -2
  150. package/dist/http/ApiRouter.d.ts.map +1 -1
  151. package/dist/http/HttpRequest.d.ts +4 -0
  152. package/dist/http/HttpRequest.d.ts.map +1 -1
  153. package/dist/http/ViewRouter.d.ts +127 -4
  154. package/dist/http/ViewRouter.d.ts.map +1 -1
  155. package/dist/http/index.d.ts +1 -0
  156. package/dist/http/index.d.ts.map +1 -1
  157. package/dist/http/index.js +2 -2
  158. package/dist/http/index.js.map +1 -1
  159. package/dist/http/middlewareList.d.ts +25 -0
  160. package/dist/http/middlewareList.d.ts.map +1 -0
  161. package/dist/http/requestContext.d.ts +57 -0
  162. package/dist/http/requestContext.d.ts.map +1 -1
  163. package/dist/i18n/dictionaryRuntime.js +2 -2
  164. package/dist/i18n/dictionaryRuntime.js.map +1 -1
  165. package/dist/i18n/index.js +2 -2
  166. package/dist/i18n/index.js.map +1 -1
  167. package/dist/ide/typescript-plugin/index.js +1206 -0
  168. package/dist/ide/typescript-plugin/index.js.map +17 -0
  169. package/dist/kernel/index.js +3 -3
  170. package/dist/kernel/index.js.map +3 -3
  171. package/dist/kernel/providers.d.ts +16 -4
  172. package/dist/kernel/providers.d.ts.map +1 -1
  173. package/dist/orm/context.d.ts +65 -0
  174. package/dist/orm/context.d.ts.map +1 -1
  175. package/dist/orm/index.js +2 -2
  176. package/dist/orm/index.js.map +1 -1
  177. package/dist/server/index.js +2 -2
  178. package/dist/server/index.js.map +1 -1
  179. package/dist/services/discovery.d.ts +27 -0
  180. package/dist/services/discovery.d.ts.map +1 -1
  181. package/dist/services/events/Event.d.ts +208 -0
  182. package/dist/services/events/Event.d.ts.map +1 -0
  183. package/dist/services/events/EventManager.d.ts +285 -0
  184. package/dist/services/events/EventManager.d.ts.map +1 -0
  185. package/dist/services/events/EventServiceProvider.d.ts +46 -0
  186. package/dist/services/events/EventServiceProvider.d.ts.map +1 -0
  187. package/dist/services/events/FakeEventManager.d.ts +155 -0
  188. package/dist/services/events/FakeEventManager.d.ts.map +1 -0
  189. package/dist/services/events/FakeEventManager.test-d.d.ts +2 -0
  190. package/dist/services/events/FakeEventManager.test-d.d.ts.map +1 -0
  191. package/dist/services/events/Listener.d.ts +189 -0
  192. package/dist/services/events/Listener.d.ts.map +1 -0
  193. package/dist/services/events/Listener.test-d.d.ts +2 -0
  194. package/dist/services/events/Listener.test-d.d.ts.map +1 -0
  195. package/dist/services/events/config.d.ts +55 -0
  196. package/dist/services/events/config.d.ts.map +1 -0
  197. package/dist/services/events/listenerJob.d.ts +43 -0
  198. package/dist/services/events/listenerJob.d.ts.map +1 -0
  199. package/dist/services/features/FeatureFlagStore.d.ts +60 -0
  200. package/dist/services/features/FeatureFlagStore.d.ts.map +1 -0
  201. package/dist/services/features/FeatureManager.d.ts +82 -0
  202. package/dist/services/features/FeatureManager.d.ts.map +1 -0
  203. package/dist/services/features/FeaturesServiceProvider.d.ts +6 -0
  204. package/dist/services/features/FeaturesServiceProvider.d.ts.map +1 -0
  205. package/dist/services/features/bucket.d.ts +57 -0
  206. package/dist/services/features/bucket.d.ts.map +1 -0
  207. package/dist/services/features/config.d.ts +57 -0
  208. package/dist/services/features/config.d.ts.map +1 -0
  209. package/dist/services/features/context.d.ts +31 -0
  210. package/dist/services/features/context.d.ts.map +1 -0
  211. package/dist/services/features/defineFeature.d.ts +145 -0
  212. package/dist/services/features/defineFeature.d.ts.map +1 -0
  213. package/dist/services/features/evaluate.d.ts +74 -0
  214. package/dist/services/features/evaluate.d.ts.map +1 -0
  215. package/dist/services/features/sources/DatabaseFeatureFlagSource.d.ts +19 -0
  216. package/dist/services/features/sources/DatabaseFeatureFlagSource.d.ts.map +1 -0
  217. package/dist/services/features/sources/FeatureFlagSource.d.ts +30 -0
  218. package/dist/services/features/sources/FeatureFlagSource.d.ts.map +1 -0
  219. package/dist/services/features/sources/StaticFeatureFlagSource.d.ts +24 -0
  220. package/dist/services/features/sources/StaticFeatureFlagSource.d.ts.map +1 -0
  221. package/dist/services/features/types.d.ts +63 -0
  222. package/dist/services/features/types.d.ts.map +1 -0
  223. package/dist/services/index.d.ts +18 -1
  224. package/dist/services/index.d.ts.map +1 -1
  225. package/dist/services/index.js +8 -8
  226. package/dist/services/index.js.map +8 -4
  227. package/dist/services/queue/QueueManager.d.ts +31 -0
  228. package/dist/services/queue/QueueManager.d.ts.map +1 -1
  229. package/dist/services/router/ViewRouteDispatcher.d.ts +8 -0
  230. package/dist/services/router/ViewRouteDispatcher.d.ts.map +1 -1
  231. package/dist/services/router/createFlatViewRoutes.d.ts +14 -0
  232. package/dist/services/router/createFlatViewRoutes.d.ts.map +1 -1
  233. package/dist/services/router/streamQueryInjection.d.ts.map +1 -1
  234. package/dist/support/index.js +2 -2
  235. package/dist/support/index.js.map +1 -1
  236. package/dist/testing/Page.d.ts +12 -0
  237. package/dist/testing/Page.d.ts.map +1 -1
  238. package/dist/testing/index.d.ts +24 -0
  239. package/dist/testing/index.d.ts.map +1 -1
  240. package/dist/testing/index.js +7 -3
  241. package/dist/testing/index.js.map +1 -1
  242. package/ide/typescript-plugin/package.json +5 -0
  243. package/package.json +4 -2
  244. package/dist/chunk-4t80js0n.js +0 -33
  245. package/dist/chunk-4t80js0n.js.map +0 -18
  246. package/dist/chunk-98a576s9.js +0 -5
  247. package/dist/chunk-98a576s9.js.map +0 -30
  248. package/dist/chunk-b50zmz3t.js +0 -5
  249. package/dist/chunk-cv9w5cmb.js +0 -6
  250. package/dist/chunk-f9mfw82d.js +0 -5
  251. package/dist/chunk-f9mfw82d.js.map +0 -12
  252. package/dist/chunk-kry5vwam.js +0 -9
  253. package/dist/chunk-tmnhkphv.js +0 -19
  254. package/dist/chunk-vkngcrzq.js +0 -4
  255. /package/dist/{chunk-62723jyy.js.map → chunk-k0fvsyeh.js.map} +0 -0
  256. /package/dist/{chunk-c75mymmq.js.map → chunk-mkfpnymy.js.map} +0 -0
  257. /package/dist/{chunk-02gdzs5t.js.map → chunk-yf7vz71n.js.map} +0 -0
@@ -0,0 +1,189 @@
1
+ import type { Event, EventClass } from "./Event";
2
+ /**
3
+ * One side effect of one event, in its own file.
4
+ *
5
+ * ```typescript
6
+ * // app/listeners/SendWelcomeEmail.ts
7
+ * export class SendWelcomeEmail extends Listener {
8
+ * static name = "SendWelcomeEmail";
9
+ * static event = UserRegistered;
10
+ *
11
+ * async handle(event: UserRegistered) {
12
+ * await Mail.send(event.email, ...);
13
+ * }
14
+ * }
15
+ * ```
16
+ *
17
+ * That is the whole of what the subsystem buys: adding a fourth side effect to
18
+ * a registration is adding a file, rather than editing the controller that
19
+ * already has three.
20
+ *
21
+ * ### Why the binding lives here and not on the event
22
+ *
23
+ * The listener is the thing with an opinion about what it cares about; an event
24
+ * has no business knowing who is watching. An event listing its listeners would
25
+ * also put the property back the way it was — adding a side effect would edit
26
+ * an existing file.
27
+ *
28
+ * ### Why the binding is a value at all
29
+ *
30
+ * Laravel binds a listener to an event by reflecting on the type-hint of
31
+ * `handle(UserRegistered $event)`. TypeScript erases types at runtime, so that
32
+ * mechanism cannot exist here and the binding has to be carried as a value.
33
+ * That constraint turns out to be a favourable one: it is also what makes the
34
+ * payload types flow without a generated registry.
35
+ *
36
+ * ### The seam, written down rather than hidden
37
+ *
38
+ * **Nothing checks that `static event` and the annotation on `handle` agree.**
39
+ * A static and an instance member cannot reference each other's types, so the
40
+ * compiler sees `static event = UserRegistered` and `handle(event: OrderPaid)`
41
+ * as two unrelated declarations. Copy a listener, change the static, forget the
42
+ * annotation, and TypeScript is satisfied while the listener receives something
43
+ * else.
44
+ *
45
+ * It is accepted because the failure is local and immediate — that one listener
46
+ * reads a field that is not there, in its own stack — rather than the
47
+ * misroute-shaped failures the rest of this subsystem is arranged against. The
48
+ * `Listener.test-d.ts` beside this file pins the two halves the compiler *does*
49
+ * enforce, so a later refactor reaching for convenience cannot widen them to
50
+ * `any` unnoticed.
51
+ */
52
+ export declare abstract class Listener {
53
+ /**
54
+ * The name this listener is reported and de-duplicated under. Required.
55
+ *
56
+ * Two listener classes claiming one name is refused at registration, and a
57
+ * discovery walk makes that ordinary: `auth/NotifyAdmins.ts` beside
58
+ * `billing/NotifyAdmins.ts` is a natural thing to write, and nothing forces
59
+ * the import alias a hand-written list would have demanded.
60
+ *
61
+ * As on `Event`, the `"unset"` default is a floor and not the check — a class
62
+ * declaration always shadows it with its own implicit binding, which is the
63
+ * one a minifier renames. `discoverListeners` reads the property descriptor
64
+ * to tell a declared name from an implicit one.
65
+ *
66
+ * For a **queued** listener it is more than a label: the queue is keyed by
67
+ * name, the listener is registered under `listener:<name>`, and that string
68
+ * is what a queued dispatch carries. So a queued listener whose name is the
69
+ * implicit class binding is refused at registration rather than warned about
70
+ * — the two ends of the queue can be two different module graphs, and a name
71
+ * only one of them minified is a side effect that stops happening in
72
+ * production and reports success.
73
+ */
74
+ static name: string;
75
+ /**
76
+ * The event class this listener handles. Exactly one, required.
77
+ *
78
+ * It holds the **class**, not its name. The name is read off it once, at
79
+ * registration, inside `EventManager.useListeners` — and that read happens in
80
+ * the module graph that declared the class, so the name it yields is the
81
+ * source one. Nothing downstream keeps the class object, because a registry
82
+ * keyed by class identity is wrong in production only.
83
+ *
84
+ * There is no `static events = [A, B]`, deliberately. A listener bound to two
85
+ * events has to discriminate inside `handle`, and the natural way to write
86
+ * that is `if (event instanceof UserRegistered)` — which is `true` in every
87
+ * test and `false` in a production build, for the reason `Event`'s own doc
88
+ * comment gives. Two events wanting the same side effect are two small
89
+ * listeners calling one shared function.
90
+ *
91
+ * Declared as required, and still checked at runtime: every subclass inherits
92
+ * this declaration whether or not it assigns to it, so the compiler cannot
93
+ * see a listener that left it out. `EventManager` refuses one out loud.
94
+ */
95
+ static event: EventClass;
96
+ /**
97
+ * Where this listener runs. **A context boundary, not a performance dial.**
98
+ *
99
+ * Left `false`, the listener runs inline, inside the dispatcher's
100
+ * `kernelContext` and `ormContext`: it has the request's `app()`, the
101
+ * authenticated user through `currentActor()`, and it joins the ambient
102
+ * transaction.
103
+ *
104
+ * **Unless the event declares `static afterCommit`.** That flag releases its
105
+ * listeners after the commit and outside the transaction's scope, so a sync
106
+ * listener bound to one runs with `currentTransaction()` undefined and
107
+ * commits on its own. The listener kept sync *because* it writes a row that
108
+ * has to roll back with the write it describes — an audit trail, a ledger
109
+ * entry — starts committing separately the day someone sets that flag in the
110
+ * event's file, and nothing on this side says so: no error, no warning, and
111
+ * a `queued = false` that still reads as "inside the transaction".
112
+ *
113
+ * Set `true`, it is handed to the `QueueManager` instead and run from a
114
+ * drain, on the queue's terms. What crosses is the event's name and its
115
+ * constructor arguments as JSON, and nothing else — not the instance the sync
116
+ * listeners share, and not a line of the request. With `worker = true` it is
117
+ * a different thread with a cloned application.
118
+ *
119
+ * The part that catches people is that the context is not reliably *gone*
120
+ * either: the queue is in-process, so a drain that happens to start from
121
+ * `push` is still standing in the dispatcher's context and `app()` there
122
+ * resolves the request's application. Nothing about that is promised. A
123
+ * queued listener that reads the current actor, or writes expecting to join
124
+ * the ambient transaction, works until the day the queue was already busy —
125
+ * and then reads different rows, or commits separately, with no error either
126
+ * way. So: a listener that needs the request's context has to stay sync, and
127
+ * a listener that only needs the payload is free to queue.
128
+ *
129
+ * What it buys, in exchange, is the queue's whole retry path: `maxAttempts`,
130
+ * `onFail`-style re-queueing and dead-lettering, none of which a sync
131
+ * listener has. `dispatchAndWait` does **not** wait for it.
132
+ */
133
+ queued: boolean;
134
+ /**
135
+ * Attempts before the queue gives up, counting the first. `Job`'s field and
136
+ * `Job`'s meaning, because it is forwarded to one.
137
+ *
138
+ * **Ignored unless `queued` is true.** A sync listener has no retry path at
139
+ * all — its throw is logged and the next listener runs — so a `maxAttempts`
140
+ * beside `queued = false` is a line that does nothing, which is why it is
141
+ * said here rather than in a note somewhere else.
142
+ */
143
+ maxAttempts: number;
144
+ /**
145
+ * Runs `handle` in a Worker thread with its own cloned application, for a
146
+ * queued listener whose work is CPU-bound. `Job`'s field and `Job`'s
147
+ * meaning, because it is forwarded to one.
148
+ *
149
+ * **Ignored unless `queued` is true.** A sync listener runs on the
150
+ * dispatcher's stack by definition; there is no thread to move it to.
151
+ */
152
+ worker: boolean;
153
+ /**
154
+ * The side effect. Runs inside the dispatcher's context when `queued` is
155
+ * false: a sync listener has the request's `app()`, its authenticated user,
156
+ * and — unless the event declares `static afterCommit` — its ambient
157
+ * transaction. A queued one can rely on none of them — see `queued`.
158
+ *
159
+ * Annotate the parameter with the event named in `static event` — narrowing
160
+ * the base's `Event` here is legal because method parameters are bivariant,
161
+ * which is what lets this class avoid a generic parameter. Nothing checks
162
+ * that the two agree; see the note on the class.
163
+ *
164
+ * Abstract, so a listener that forgets it fails to compile rather than
165
+ * silently handling nothing.
166
+ *
167
+ * A throw is caught, logged with both names, and the next listener still
168
+ * runs. Listeners are independent side effects by construction, and their
169
+ * order is a filesystem walk's — letting one cancel the rest would make that
170
+ * order load-bearing, which is the exact coupling this subsystem exists to
171
+ * remove.
172
+ */
173
+ abstract handle(event: Event): void | Promise<void>;
174
+ }
175
+ /**
176
+ * A `Listener` subclass, as the registry and the `events` config slice hold
177
+ * one.
178
+ *
179
+ * Zero-argument, because the manager constructs one per dispatch and has
180
+ * nothing to pass it. The `event` member is typed as present even though
181
+ * `EventManager` checks for it at runtime: an inherited declaration is
182
+ * indistinguishable from an assignment to the type system, so this is the
183
+ * shape, and the runtime check is what covers the difference.
184
+ */
185
+ export type ListenerClass = (new () => Listener) & {
186
+ name: string;
187
+ event: EventClass;
188
+ };
189
+ //# sourceMappingURL=Listener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Listener.d.ts","sourceRoot":"","sources":["../../../services/events/Listener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,8BAAsB,QAAQ;IAC5B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,IAAI,SAAW;IAEtB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC;IAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,MAAM,UAAS;IAEf;;;;;;;;OAQG;IACH,WAAW,SAAK;IAEhB;;;;;;;OAOG;IACH,MAAM,UAAS;IAEf;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CACpD;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,UAAU,QAAQ,CAAC,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;CACnB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Listener.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Listener.test-d.d.ts","sourceRoot":"","sources":["../../../services/events/Listener.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import type { ListenerClass } from "./Listener";
2
+ export interface EventConfig {
3
+ /**
4
+ * The `Listener` subclasses this application registers, or nothing.
5
+ *
6
+ * ### Why "or nothing" is the point
7
+ *
8
+ * A dispatch fans out to whatever the `EventManager` holds for the event's
9
+ * name, and an event nothing is registered for is not an error — it is the
10
+ * ordinary state of an application that has not written that listener yet.
11
+ * So this list is a second spelling of `app/listeners`, kept in step by hand,
12
+ * and what the two disagreeing costs is a side effect that stops happening,
13
+ * with a development-only warning as the only trace. Nothing fails, nothing
14
+ * is dropped from a log: the welcome email simply is not sent.
15
+ *
16
+ * Leaving this out spells it once: the listeners are the classes under
17
+ * `listenersDir`.
18
+ *
19
+ * ### The rule, exactly
20
+ *
21
+ * **Declared wins, and `[]` is declared.** A `listeners` that is present is
22
+ * used verbatim and no directory is read — that is the escape hatch for an
23
+ * app whose listeners live somewhere this cannot walk, for one that
24
+ * deliberately registers a subset, and for a deploy that ships no source. An
25
+ * empty array means an app with no listeners and says so; it does not mean
26
+ * "find some".
27
+ *
28
+ * **Absent or `undefined` discovers.** `undefined` counts as absent for the
29
+ * same reason `withDefaults` treats it that way everywhere else: a key spread
30
+ * in from an optional value is an omission, not an instruction.
31
+ */
32
+ listeners?: ListenerClass[];
33
+ /**
34
+ * Where to look when `listeners` was not declared. Relative to the project
35
+ * root, or absolute.
36
+ *
37
+ * Every `.ts`/`.tsx` file underneath it is imported at boot and every
38
+ * exported class extending `Listener` is registered, so this wants to be a
39
+ * directory of listener declarations rather than a directory that merely
40
+ * contains some. That includes an abstract base a few listeners share: the
41
+ * walk excludes the framework's `Listener` and nothing else, so a base
42
+ * sitting here is registered alongside its subclasses and its `handle` runs
43
+ * on every dispatch of whatever event it declares. Keep shared bases outside
44
+ * this directory.
45
+ *
46
+ * There is no `eventsDir` to go with it. Event classes are never discovered
47
+ * — each is imported by the listener that binds to it and by the code that
48
+ * dispatches it, so nothing needs to walk them and a walk that imported them
49
+ * anyway would only be a boot cost.
50
+ */
51
+ listenersDir?: string;
52
+ }
53
+ export declare function defineEventConfig(config: EventConfig): EventConfig;
54
+ export declare function eventConfigDefaults(): Required<EventConfig>;
55
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../services/events/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,MAAM,WAAW,WAAW;IAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAE5B;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAElE;AAED,wBAAgB,mBAAmB,IAAI,QAAQ,CAAC,WAAW,CAAC,CAK3D"}
@@ -0,0 +1,43 @@
1
+ import { Job } from "../queue/Job";
2
+ import type { Listener, ListenerClass } from "./Listener";
3
+ /**
4
+ * The adapter that lets `queued = true` be one line: a `Job` subclass, built at
5
+ * registration, that runs exactly one listener.
6
+ *
7
+ * Retries, `maxAttempts`, dead-lettering, `concurrency` and worker-thread
8
+ * execution are four features the queue already has, already documents for
9
+ * applications, and is already tested on. A second queue for listeners would be
10
+ * four features that are almost the same as those, differing in ways nobody
11
+ * decided. So a queued listener is not queued by anything written here — it is
12
+ * a job, and everything after `push` is the queue's.
13
+ */
14
+ /**
15
+ * The name a listener's synthetic job is registered under.
16
+ *
17
+ * Prefixed, and visibly so. The synthetic jobs land in
18
+ * `QueueManager.registeredJobs` alongside the app's own, because that getter is
19
+ * documented as reporting what the manager was handed and a queued listener
20
+ * genuinely is something the queue will run — hiding them would make a queue
21
+ * introspection tool lie about what is about to execute. The prefix is what
22
+ * keeps an author from reading `SendWelcomeEmail` there and looking for a job
23
+ * file they never wrote.
24
+ */
25
+ export declare function listenerJobName(listenerName: string): string;
26
+ /**
27
+ * Builds the `Job` subclass that runs one queued listener.
28
+ *
29
+ * `instance` is the listener the caller already constructed to read `queued`
30
+ * off; the three fields that decide where the work runs are read from that one
31
+ * instance rather than from a fresh one per attempt, so a listener's
32
+ * constructor runs once at boot instead of once per retry.
33
+ *
34
+ * What crosses the queue is `[eventName, constructorArguments]` as JSON — a
35
+ * name and the arguments, never the event instance and never the classes. That
36
+ * is invariant 1 at its second boundary: the class objects on the pushing side
37
+ * and the running side can come from two different module graphs (a minified
38
+ * bundle and a source-side discovery walk), so a name declared as a string
39
+ * literal is the only thing both ends agree on. `EventManager.rehydrate` turns
40
+ * it back into an event.
41
+ */
42
+ export declare function jobForListener(listener: ListenerClass, instance: Listener): new () => Job;
43
+ //# sourceMappingURL=listenerJob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listenerJob.d.ts","sourceRoot":"","sources":["../../../services/events/listenerJob.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAEnC,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;;GAUG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,QAAQ,GACjB,UAAU,GAAG,CAiFf"}
@@ -0,0 +1,60 @@
1
+ import type { FeatureRegistry } from "./defineFeature";
2
+ import type { FeatureFlagSource } from "./sources/FeatureFlagSource";
3
+ export type Warn = (message: string) => void;
4
+ export interface FlagSnapshot {
5
+ /** `key -> active`. A key absent from the map has no row, and is off. */
6
+ active: Map<string, boolean>;
7
+ loadedAt: number;
8
+ /** True only while nothing has *ever* loaded successfully. */
9
+ unavailable: boolean;
10
+ }
11
+ /**
12
+ * The process-local cache of on/off switches.
13
+ *
14
+ * gemi has no `Cache` facade, and this does not add one — a general cache
15
+ * abstraction is a much larger design, and coupling features to a hypothetical
16
+ * version of it would block both.
17
+ *
18
+ * Three properties, each load-bearing:
19
+ *
20
+ * **Stale-while-revalidate.** After the first load, `get()` returns an
21
+ * already-resolved snapshot and refreshes in the background. No request ever
22
+ * waits on the database for a feature. This is what makes evaluating every
23
+ * feature on every request affordable, and it is why the manager can be eager.
24
+ *
25
+ * **Single-flight.** A cold start under load must not issue one query per
26
+ * in-flight request, so concurrent refreshes share one promise.
27
+ *
28
+ * **A failed refresh keeps the last good data.** An outage must not read as
29
+ * "every feature switched itself off" — that is a config change nobody made,
30
+ * applied to production, at the exact moment something else is already broken.
31
+ */
32
+ export declare class FeatureFlagStore {
33
+ private readonly source;
34
+ private readonly declared;
35
+ private readonly ttlMs;
36
+ private readonly warn;
37
+ private snapshot;
38
+ private inflight;
39
+ /** Rate-limits the "could not load" line to once per TTL window. */
40
+ private lastFailureLoggedAt;
41
+ constructor(source: FeatureFlagSource, declared: FeatureRegistry, ttlMs: number, warn?: Warn);
42
+ /** Never rejects. Callers always get a snapshot, possibly an empty one. */
43
+ get(): Promise<FlagSnapshot>;
44
+ /** Forces a reload now, sharing one query across concurrent callers. */
45
+ refresh(): Promise<FlagSnapshot>;
46
+ /** What is cached right now, without triggering a load. */
47
+ peek(): FlagSnapshot | null;
48
+ private load;
49
+ /**
50
+ * Rows in, `key -> active` out.
51
+ *
52
+ * The row is treated as hostile input — not because anyone expects it to be
53
+ * malformed, but because it is the one part of this system nobody reviews
54
+ * before it reaches production. A bad row is logged and skipped, never thrown:
55
+ * a typo in a column must not take the process down at boot.
56
+ */
57
+ private readSwitches;
58
+ private handleFailure;
59
+ }
60
+ //# sourceMappingURL=FeatureFlagStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureFlagStore.d.ts","sourceRoot":"","sources":["../../../services/features/FeatureFlagStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAErE,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAgB;IAOzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IATvB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,QAAQ,CAAsC;IACtD,oEAAoE;IACpE,OAAO,CAAC,mBAAmB,CAAK;gBAGb,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,IAAe;IAGxC,2EAA2E;IACrE,GAAG,IAAI,OAAO,CAAC,YAAY,CAAC;IAiBlC,wEAAwE;IACxE,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC;IAOhC,2DAA2D;IAC3D,IAAI,IAAI,YAAY,GAAG,IAAI;YAIb,IAAI;IAclB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,aAAa;CAkBtB"}
@@ -0,0 +1,82 @@
1
+ import type { ResolvedFeaturesConfig } from "./config";
2
+ import { type FeatureSubject } from "./context";
3
+ import type { FeatureRegistry } from "./defineFeature";
4
+ import { FeatureFlagStore } from "./FeatureFlagStore";
5
+ import type { FeatureContext, FeatureEvaluation } from "./types";
6
+ /**
7
+ * Evaluation bound to one explicit context — what `Features.for(...)` returns.
8
+ *
9
+ * Exists because a job, a cron tick or an admin preview needs to ask "what would
10
+ * *this* user see", and the ambient-request path cannot answer that.
11
+ */
12
+ export declare class FeatureScope {
13
+ private readonly manager;
14
+ private readonly ctx;
15
+ constructor(manager: FeatureManager, ctx: FeatureContext);
16
+ enabled(key: string): Promise<boolean>;
17
+ explain(key: string): Promise<FeatureEvaluation>;
18
+ all(): Promise<Record<string, boolean>>;
19
+ }
20
+ export declare class FeatureManager {
21
+ readonly config: ResolvedFeaturesConfig;
22
+ private readonly log;
23
+ static token: string;
24
+ readonly store: FeatureFlagStore;
25
+ private readonly declared;
26
+ private warnedAboutSize;
27
+ constructor(config: ResolvedFeaturesConfig, log?: (message: string) => void);
28
+ /** The declarations, for a CLI or an admin surface. Server-side only. */
29
+ declarations(): FeatureRegistry;
30
+ /** Reloads the snapshot in this process now. */
31
+ refresh(): Promise<void>;
32
+ enabled(key: string): Promise<boolean>;
33
+ /**
34
+ * Value plus why. Server-side only — `reason` says whether the viewer landed
35
+ * in a rollout or was targeted by name, and must never be serialized.
36
+ */
37
+ explain(key: string): Promise<FeatureEvaluation>;
38
+ /** Every declared feature for the ambient request, as `key -> boolean`. */
39
+ all(): Promise<Record<string, boolean>>;
40
+ /**
41
+ * What the SSR payload carries: client-visible features only.
42
+ *
43
+ * The single function the dispatcher calls, and the only place the server-only
44
+ * exclusion is applied — so "what reaches the browser" has one answer in one
45
+ * place rather than a rule each caller has to remember.
46
+ */
47
+ forClient(): Promise<Record<string, boolean>>;
48
+ /** Evaluation against an explicit subject, for jobs, cron and previews. */
49
+ for(subject: FeatureSubject): FeatureScope;
50
+ /**
51
+ * @internal — shared by the ambient path and `FeatureScope`. The single place
52
+ * a feature is evaluated, and therefore the single place the per-request memo
53
+ * is consulted and `onEvaluate` fires.
54
+ *
55
+ * Both of those used to live in `explain()`, one level up, which meant every
56
+ * caller that did not go through it — `forClient()` building the SSR payload,
57
+ * `passesFeatureGates` checking a route — evaluated afresh and notified again.
58
+ * A request that read a feature in a handler and then rendered it emitted two
59
+ * exposures for one viewer, quietly doubling whatever counted them.
60
+ */
61
+ evaluateIn(key: string, ctx: FeatureContext, buckets: Map<string, number>): Promise<FeatureEvaluation>;
62
+ /** @internal */
63
+ evaluateAllIn(ctx: FeatureContext, buckets: Map<string, number>, options: {
64
+ clientOnly: boolean;
65
+ }): Promise<Record<string, boolean>>;
66
+ private switchFor;
67
+ private requestContext;
68
+ /**
69
+ * The request's evaluation memo, or `null` when there is none to use.
70
+ *
71
+ * Gated on the context being the ambient request's *own*, by identity. A
72
+ * `Features.for({ user })` inside a request is asking what somebody else would
73
+ * see, and answering it from — or writing it into — the current viewer's memo
74
+ * would cross the two: an admin previewing a customer would poison every
75
+ * subsequent read on that request with the customer's values.
76
+ */
77
+ private memoFor;
78
+ private requestBuckets;
79
+ private notify;
80
+ private warnIfOversized;
81
+ }
82
+ //# sourceMappingURL=FeatureManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureManager.d.ts","sourceRoot":"","sources":["../../../services/features/FeatureManager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAA0C,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AACxF,OAAO,KAAK,EAAW,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjE;;;;;GAKG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,cAAc;IAGhC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAItC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIhD,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAK9C;AAED,qBAAa,cAAc;IAQvB,QAAQ,CAAC,MAAM,EAAE,sBAAsB;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,MAAM,CAAC,KAAK,SAAc;IAE1B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,eAAe,CAAS;gBAGrB,MAAM,EAAE,sBAAsB,EACtB,GAAG,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAe;IAe5D,yEAAyE;IACzE,YAAY,IAAI,eAAe;IAI/B,gDAAgD;IAC1C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAUxB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI5C;;;OAGG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAKtD,2EAA2E;IACrE,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAO7C;;;;;;OAMG;IACG,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAWnD,2EAA2E;IAC3E,GAAG,CAAC,OAAO,EAAE,cAAc,GAAG,YAAY;IAI1C;;;;;;;;;;OAUG;IACG,UAAU,CACd,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,OAAO,CAAC,iBAAiB,CAAC;IAyB7B,gBAAgB;IACV,aAAa,CACjB,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE;QAAE,UAAU,EAAE,OAAO,CAAA;KAAE,GAC/B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAWrB,SAAS;YAgBT,cAAc;IAS5B;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,MAAM;IAcd,OAAO,CAAC,eAAe;CAOxB"}
@@ -0,0 +1,6 @@
1
+ import { ServiceProvider } from "../../support/ServiceProvider";
2
+ export declare class FeaturesServiceProvider extends ServiceProvider {
3
+ register(): void;
4
+ boot(): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=FeaturesServiceProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeaturesServiceProvider.d.ts","sourceRoot":"","sources":["../../../services/features/FeaturesServiceProvider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAMhE,qBAAa,uBAAwB,SAAQ,eAAe;IAC1D,QAAQ;IAqCF,IAAI;CAkBX"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Deterministic bucketing for percentage rollouts.
3
+ *
4
+ * ## Why nothing is stored
5
+ *
6
+ * A subject's bucket is a pure function of the feature and the subject, so it is
7
+ * the same on every machine, in every process, on every device that subject logs
8
+ * in from, forever — without a row, a cookie, or a cache holding the assignment.
9
+ * Persisting assignments would be strictly worse: it would be per-browser rather
10
+ * than per-subject, it would need migrating whenever a feature changes, and it
11
+ * would grow with the user count.
12
+ *
13
+ * ## Why SHA-1 and not `Bun.hash`
14
+ *
15
+ * `Bun.hash` is faster and wrong for this. Its output is seeded and is not a
16
+ * documented stable contract across Bun versions, so a runtime upgrade could
17
+ * silently re-bucket every subject in the middle of a rollout — the 10% who had
18
+ * the new checkout become a different 10%, and nobody finds out from a stack
19
+ * trace. SHA-1 is a fixed standard: the same string maps to the same bucket on
20
+ * every machine, every process and every version. This is a correctness
21
+ * requirement, not a security one, so SHA-1's collision weakness is irrelevant
22
+ * here — and `Bun.CryptoHasher("sha1")` is already how `server/generateEtag.ts`
23
+ * hashes.
24
+ *
25
+ * ## What is in the key
26
+ *
27
+ * `salt:subject`, where `salt` defaults to the feature's key.
28
+ *
29
+ * The salt is what decorrelates features from each other. Without it two
30
+ * independent 20% rollouts would select the *same* 20% of subjects, so a user
31
+ * unlucky once would be unlucky in everything and the two populations could
32
+ * never be reasoned about separately.
33
+ */
34
+ declare const RESOLUTION = 10000;
35
+ /** The bucket key. Exported so a test can pin exact strings to exact buckets. */
36
+ export declare function bucketKey(salt: string, subject: string): string;
37
+ /**
38
+ * A stable integer in `[0, RESOLUTION)` — i.e. 0.01% granularity.
39
+ *
40
+ * Takes the first 32 bits of the digest. That is four orders of magnitude more
41
+ * entropy than the 10,000 buckets it is reduced to, so the modulo bias is far
42
+ * below the noise floor of any rollout anyone would configure.
43
+ */
44
+ export declare function bucketOf(key: string): number;
45
+ /**
46
+ * Whether `bucket` falls inside `percent`.
47
+ *
48
+ * `bucket < threshold` rather than a range test, which buys monotonicity for
49
+ * free: a subject inside a 10% rollout is still inside the same feature's 20%
50
+ * rollout. Ramping up therefore only ever *adds* people. The alternative —
51
+ * anything that reshuffles on each change — means a user who saw the feature at
52
+ * 10% can lose it at 20%, which reads as a bug to them and invalidates any
53
+ * measurement taken across the change.
54
+ */
55
+ export declare function inRollout(bucket: number, percent: number): boolean;
56
+ export { RESOLUTION };
57
+ //# sourceMappingURL=bucket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bucket.d.ts","sourceRoot":"","sources":["../../../services/features/bucket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,QAAA,MAAM,UAAU,QAAS,CAAC;AAE1B,iFAAiF;AACjF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI5C;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAIlE;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { HttpRequest } from "../../http/HttpRequest";
2
+ import type { FeatureRegistry } from "./defineFeature";
3
+ import type { FeatureFlagSource } from "./sources/FeatureFlagSource";
4
+ import type { FeatureContext, FeatureEvaluation } from "./types";
5
+ export interface FeaturesConfig {
6
+ /**
7
+ * The application's declarations — the default export of
8
+ * `app/features/index.ts`.
9
+ *
10
+ * Named here as well as in the framework's `gemi.d.ts` because the two layers
11
+ * cannot share one reference: the type augmentation resolves `@/app/features`
12
+ * through the app's tsconfig paths, which exists only at compile time. This is
13
+ * the runtime half. They should point at the same object.
14
+ */
15
+ features?: FeatureRegistry;
16
+ /**
17
+ * Master switch. `false` skips loading and evaluation entirely: every feature
18
+ * reads off and the SSR payload carries `{}`.
19
+ */
20
+ enabled?: boolean;
21
+ /** Where the on/off switches come from. The database by default. */
22
+ source?: FeatureFlagSource;
23
+ /** ORM registry name of the model, when using the database source. */
24
+ model?: string;
25
+ /**
26
+ * Snapshot lifetime in **seconds**.
27
+ *
28
+ * This is the propagation delay: switching a feature on or off is live on
29
+ * every instance within this window. Lower it if that matters more than the
30
+ * query volume — there is no cross-instance invalidation.
31
+ */
32
+ ttl?: number;
33
+ /**
34
+ * Extra attributes for every evaluation — country, cohort, build, plan.
35
+ * Reachable in a `when` as `ctx.attributes`.
36
+ *
37
+ * Runs on every request inside the render path, so keep it cheap and free of
38
+ * I/O. If it throws, evaluation degrades to no attributes rather than failing
39
+ * the page.
40
+ */
41
+ context?: (req: HttpRequest | null) => Record<string, unknown> | Promise<Record<string, unknown>>;
42
+ /**
43
+ * Called once per key per request after evaluation — the hook an analytics or
44
+ * experiment pipeline reads exposures from. Errors are caught and logged.
45
+ */
46
+ onEvaluate?: (key: string, evaluation: FeatureEvaluation, ctx: FeatureContext) => void;
47
+ /** Warn once per boot above this many client-visible features. */
48
+ maxClientFlags?: number;
49
+ }
50
+ export declare function defineFeaturesConfig(config: FeaturesConfig): FeaturesConfig;
51
+ export declare function featuresConfigDefaults(): Required<Omit<FeaturesConfig, "features" | "context" | "onEvaluate">> & {
52
+ features: FeaturesConfig["features"];
53
+ context: FeaturesConfig["context"];
54
+ onEvaluate: FeaturesConfig["onEvaluate"];
55
+ };
56
+ export type ResolvedFeaturesConfig = ReturnType<typeof featuresConfigDefaults>;
57
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../services/features/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGjE,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;IAE3B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,oEAAoE;IACpE,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAE3B,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAElG;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IAEvF,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAE3E;AAED,wBAAgB,sBAAsB,IAAI,QAAQ,CAChD,IAAI,CAAC,cAAc,EAAE,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC,CAC5D,GAAG;IACF,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACrC,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACnC,UAAU,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;CAC1C,CAWA;AAED,MAAM,MAAM,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { ResolvedFeaturesConfig } from "./config";
2
+ import type { FeatureContext } from "./types";
3
+ export interface FeatureSubject {
4
+ user?: Record<string, any> | null;
5
+ attributes?: Record<string, unknown>;
6
+ /** The bucketing subject when there is no user — an org id, a device id. */
7
+ subjectId?: string;
8
+ }
9
+ /**
10
+ * Builds the context a feature is evaluated against from the ambient request.
11
+ *
12
+ * Two decisions worth stating:
13
+ *
14
+ * **The user comes from the request store, not `Auth.user()`.** `Auth.user()`
15
+ * throws `AuthenticationError` when nobody is signed in and performs a session
16
+ * lookup to find out. Features have to be evaluable on an anonymous marketing
17
+ * page without either, so this reads `store.user` and accepts `null`. The
18
+ * consequence is worth knowing: on a route with no `auth` middleware, where
19
+ * nothing has resolved a session, `ctx.user` is `null` and a `when` that reads
20
+ * it will not match. The fix an application reaches for is the `auth` middleware
21
+ * it already has.
22
+ *
23
+ * **No request is not an error.** A job, a cron tick or a console command has no
24
+ * store, and `Features.enabled()` there answers with an anonymous context rather
25
+ * than throwing. `Features.for({ user })` is the explicit form when a background
26
+ * task needs to evaluate as somebody.
27
+ */
28
+ export declare function contextFromRequest(config: ResolvedFeaturesConfig, warn?: (message: string) => void): Promise<FeatureContext>;
29
+ /** The same shape, from an explicit subject rather than the ambient request. */
30
+ export declare function contextFromSubject(subject: FeatureSubject): FeatureContext;
31
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../services/features/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,sBAAsB,EAC9B,IAAI,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAe,GACzC,OAAO,CAAC,cAAc,CAAC,CAyBzB;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAc1E"}