@edge-base/server 0.2.4 → 0.2.5

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 (68) hide show
  1. package/admin-build/_app/immutable/chunks/{Dj-E9-FO.js → 6oMK_164.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{BME_U9TJ.js → B2TnDKF7.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{Dj0QUuOf.js → B6MschND.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{fYEKMQ-Z.js → B94PilAN.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{C6lpZLE2.js → BEW7Ez_g.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{BYI6CUvd.js → BoOooyH6.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{5RQRbp5q.js → BqTb6Mxk.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{wCNueVYy.js → BvHnF5tV.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/{XQM1k9PM.js → CaVKAiCe.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{BjWZuf8W.js → Cdm5zBRA.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/CrOZMmdF.js +1 -0
  12. package/admin-build/_app/immutable/chunks/Cw6OYcq-.js +1 -0
  13. package/admin-build/_app/immutable/chunks/{BgDzp0i0.js → D2j3I1VQ.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{D5GswVnI.js → DPdQ7z0T.js} +3 -3
  15. package/admin-build/_app/immutable/chunks/{DYaCRWMA.js → J2Gw0SMu.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{g_-Kpxu3.js → pUxw8jfq.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.C8ylfBe6.js → app.D3flihMw.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.Cl6sLxnz.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.CJJ6HZbp.js → 0.CdczqZLK.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.B4sI5cB4.js → 1.DxcSsEqS.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.D6hvCer6.js → 10.DuAd4aIm.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.Dx7b8aQ5.js → 11.0jgHQL92.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.Bqmy5KIF.js → 12.CKNPqmyy.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.CC6KpXgS.js → 13.B1p2POXS.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.yCo1Ix8E.js → 14.Bb-REBND.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.co0UfPlh.js → 15.1uBFCX0X.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.D0xkPUBW.js → 16.BR7WwQrS.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.CebNqPeh.js → 17.Cm57KKXV.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.JUoLOZxh.js → 18.CoiwfAuQ.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.ND8kmQJe.js → 19.B8ZdLlXj.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.DYb-q3W8.js → 20.DnHeFlTv.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.CJFaf0Ia.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.UOzm8WYV.js → 22.CItETFzy.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.BLgq21om.js → 23.CWSGMcKJ.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.DN9usmUs.js → 24.CWbEqNMB.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.BddRfAyE.js → 25.DRkLEhKi.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.Dl6XHIeT.js → 26.BRxO8AYH.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.D0iNwALG.js → 27.BLs-nVHz.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.9dKQmdGi.js → 28.G79qkdBK.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.wXzfJUXp.js → 29.BOcI6g0N.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.z8ut3jS-.js → 3.B6q-7qr8.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.BtZETNsL.js → 30.DAIC7dKd.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.CYonj2Jh.js → 31.pl0XXjXF.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.COtDPQ9b.js → 4.DOdvVlZj.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.CTRCeIhp.js → 5.BW_zlgye.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.ChHi3QkR.js → 6.Dxy1CAI2.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.CCMtr6Ac.js → 7.BG98w_o7.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.DpWJ-X_-.js → 8.DoG5R2rG.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.DOkvfmir.js → 9.Dmxf6zAC.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -3
  53. package/src/__tests__/database-do-route-validation.test.ts +10 -7
  54. package/src/__tests__/meta-route-registration.test.ts +20 -15
  55. package/src/__tests__/room-auth-state-loss.test.ts +122 -0
  56. package/src/__tests__/room-handler-context.test.ts +4 -4
  57. package/src/__tests__/room-rate-limit-scopes.test.ts +38 -0
  58. package/src/__tests__/runtime-startup.test.ts +49 -0
  59. package/src/durable-objects/database-do.ts +14 -0
  60. package/src/durable-objects/database-live-do.ts +15 -0
  61. package/src/durable-objects/room-runtime-base.ts +387 -129
  62. package/src/durable-objects/rooms-do.ts +31 -24
  63. package/src/index.ts +326 -280
  64. package/src/lib/runtime-startup.ts +53 -0
  65. package/admin-build/_app/immutable/chunks/DBsVqhuh.js +0 -1
  66. package/admin-build/_app/immutable/chunks/D__dwMuW.js +0 -1
  67. package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +0 -1
  68. package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +0 -1
@@ -1 +1 @@
1
- import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/C6lpZLE2.js";import{a as F,f as I}from"../chunks/DEELgv7K.js";import{s as Y}from"../chunks/CoI6jjbg.js";import{P as Z}from"../chunks/B8s_s9QY.js";import{a as tt}from"../chunks/Q2nPFxS6.js";import{M as _,T as et}from"../chunks/5RQRbp5q.js";import{D as at,T as rt}from"../chunks/BgDzp0i0.js";var nt=I("<button> </button>"),st=I('<div class="range-selector"></div>'),ot=I('<div class="analytics-grid"><!> <!> <!> <!></div> <div class="analytics-chart"><!></div> <div class="analytics-bottom"><!> <!></div>',1);function bt(w,M){j(M,!0);let s=D(!0),o=D(null),p=D("24h");const T=[{value:"1h",label:"1H"},{value:"6h",label:"6H"},{value:"24h",label:"24H"},{value:"7d",label:"7D"},{value:"30d",label:"30D"}],k=n(()=>()=>{switch(t(p)){case"1h":return"minute";case"6h":case"24h":return"hour";default:return"day"}});async function $(){try{const e=await Q.fetch(`data/analytics?range=${t(p)}&category=function&metric=overview&groupBy=${t(k)()}`);y(o,e,!0)}catch(e){V(e instanceof Error?e.message:"Failed to load functions analytics")}finally{y(s,!1)}}function A(e){y(p,e,!0),y(s,!0),$()}U(()=>{$();const e=setInterval($,3e4);return()=>clearInterval(e)});const H=n(()=>()=>{var c;if(!((c=t(o))!=null&&c.summary))return 0;const{totalRequests:e,totalErrors:a}=t(o).summary;return e>0?a/e*100:0}),B=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.timeSeries)||[]).map(a=>({timestamp:a.timestamp,value:a.requests??a.value??0}))}),C=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.breakdown)||[]).map(a=>({label:a.label||"other",value:a.count}))}),E=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.topItems)||[]).map(a=>({label:a.label,count:a.count,secondary:`${Math.round(a.avgLatency)}ms · ${a.errorRate.toFixed(1)}% 5xx`}))});Z(w,{title:"Functions Analytics",description:"Serverless function execution and performance metrics",get docsHref(){return tt},actions:a=>{var c=st();W(c,21,()=>T,X,(f,d)=>{var l=nt();let v;var h=b(l,!0);g(l),J(()=>{v=Y(l,1,"range-btn",null,v,{"range-btn--active":t(p)===t(d).value}),N(h,t(d).label)}),O("click",l,()=>A(t(d).value)),F(f,l)}),g(c),F(a,c)},children:(a,c)=>{var f=ot(),d=G(f),l=b(d);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.totalRequests)??0});_(l,{label:"Invocations",get value(){return t(r)},get loading(){return t(s)}})}var v=m(l,2);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.uniqueUsers)??0});_(v,{label:"Unique Users",get value(){return t(r)},get loading(){return t(s)}})}var h=m(v,2);{let r=n(()=>t(H)().toFixed(1));_(h,{label:"5xx Rate",get value(){return t(r)},unit:"%",get loading(){return t(s)}})}var L=m(h,2);{let r=n(()=>{var i,u;return Math.round(((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.avgLatency)??0)});_(L,{label:"Avg Duration",get value(){return t(r)},unit:"ms",get loading(){return t(s)}})}g(d);var x=m(d,2),P=b(x);{let r=n(()=>t(B)());et(P,{get data(){return t(r)},type:"bar",height:240,label:"Function invocations over time",get loading(){return t(s)},color:"#dc2626"})}g(x);var R=m(x,2),q=b(R);{let r=n(()=>t(C)());at(q,{get items(){return t(r)},title:"Functions by invocation count",get loading(){return t(s)}})}var S=m(q,2);{let r=n(()=>t(E)());rt(S,{get items(){return t(r)},title:"Top Functions",get loading(){return t(s)}})}g(R),F(a,f)}}),z()}K(["click"]);export{bt as component};
1
+ import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/BEW7Ez_g.js";import{a as F,f as I}from"../chunks/DEELgv7K.js";import{s as Y}from"../chunks/CoI6jjbg.js";import{P as Z}from"../chunks/B8s_s9QY.js";import{a as tt}from"../chunks/Q2nPFxS6.js";import{M as _,T as et}from"../chunks/BqTb6Mxk.js";import{D as at,T as rt}from"../chunks/D2j3I1VQ.js";var nt=I("<button> </button>"),st=I('<div class="range-selector"></div>'),ot=I('<div class="analytics-grid"><!> <!> <!> <!></div> <div class="analytics-chart"><!></div> <div class="analytics-bottom"><!> <!></div>',1);function bt(w,M){j(M,!0);let s=D(!0),o=D(null),p=D("24h");const T=[{value:"1h",label:"1H"},{value:"6h",label:"6H"},{value:"24h",label:"24H"},{value:"7d",label:"7D"},{value:"30d",label:"30D"}],k=n(()=>()=>{switch(t(p)){case"1h":return"minute";case"6h":case"24h":return"hour";default:return"day"}});async function $(){try{const e=await Q.fetch(`data/analytics?range=${t(p)}&category=function&metric=overview&groupBy=${t(k)()}`);y(o,e,!0)}catch(e){V(e instanceof Error?e.message:"Failed to load functions analytics")}finally{y(s,!1)}}function A(e){y(p,e,!0),y(s,!0),$()}U(()=>{$();const e=setInterval($,3e4);return()=>clearInterval(e)});const H=n(()=>()=>{var c;if(!((c=t(o))!=null&&c.summary))return 0;const{totalRequests:e,totalErrors:a}=t(o).summary;return e>0?a/e*100:0}),B=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.timeSeries)||[]).map(a=>({timestamp:a.timestamp,value:a.requests??a.value??0}))}),C=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.breakdown)||[]).map(a=>({label:a.label||"other",value:a.count}))}),E=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.topItems)||[]).map(a=>({label:a.label,count:a.count,secondary:`${Math.round(a.avgLatency)}ms · ${a.errorRate.toFixed(1)}% 5xx`}))});Z(w,{title:"Functions Analytics",description:"Serverless function execution and performance metrics",get docsHref(){return tt},actions:a=>{var c=st();W(c,21,()=>T,X,(f,d)=>{var l=nt();let v;var h=b(l,!0);g(l),J(()=>{v=Y(l,1,"range-btn",null,v,{"range-btn--active":t(p)===t(d).value}),N(h,t(d).label)}),O("click",l,()=>A(t(d).value)),F(f,l)}),g(c),F(a,c)},children:(a,c)=>{var f=ot(),d=G(f),l=b(d);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.totalRequests)??0});_(l,{label:"Invocations",get value(){return t(r)},get loading(){return t(s)}})}var v=m(l,2);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.uniqueUsers)??0});_(v,{label:"Unique Users",get value(){return t(r)},get loading(){return t(s)}})}var h=m(v,2);{let r=n(()=>t(H)().toFixed(1));_(h,{label:"5xx Rate",get value(){return t(r)},unit:"%",get loading(){return t(s)}})}var L=m(h,2);{let r=n(()=>{var i,u;return Math.round(((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.avgLatency)??0)});_(L,{label:"Avg Duration",get value(){return t(r)},unit:"ms",get loading(){return t(s)}})}g(d);var x=m(d,2),P=b(x);{let r=n(()=>t(B)());et(P,{get data(){return t(r)},type:"bar",height:240,label:"Function invocations over time",get loading(){return t(s)},color:"#dc2626"})}g(x);var R=m(x,2),q=b(R);{let r=n(()=>t(C)());at(q,{get items(){return t(r)},title:"Functions by invocation count",get loading(){return t(s)}})}var S=m(q,2);{let r=n(()=>t(E)());rt(S,{get items(){return t(r)},title:"Top Functions",get loading(){return t(s)}})}g(R),F(a,f)}}),z()}K(["click"]);export{bt as component};
@@ -1 +1 @@
1
- {"version":"1774395752593"}
1
+ {"version":"1774526372532"}
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>EdgeBase Admin</title>
7
7
  <link rel="icon" href="/admin/favicon.svg" type="image/svg+xml" />
8
- <link href="/admin/_app/immutable/entry/start.CtsqDyfj.js" rel="modulepreload">
9
- <link href="/admin/_app/immutable/chunks/D__dwMuW.js" rel="modulepreload">
8
+ <link href="/admin/_app/immutable/entry/start.Cl6sLxnz.js" rel="modulepreload">
9
+ <link href="/admin/_app/immutable/chunks/CrOZMmdF.js" rel="modulepreload">
10
10
  <link href="/admin/_app/immutable/chunks/BdTBlfLy.js" rel="modulepreload">
11
11
  <link href="/admin/_app/immutable/chunks/Bn2NtlTj.js" rel="modulepreload">
12
- <link href="/admin/_app/immutable/chunks/DBsVqhuh.js" rel="modulepreload">
13
- <link href="/admin/_app/immutable/entry/app.C8ylfBe6.js" rel="modulepreload">
12
+ <link href="/admin/_app/immutable/chunks/Cw6OYcq-.js" rel="modulepreload">
13
+ <link href="/admin/_app/immutable/entry/app.D3flihMw.js" rel="modulepreload">
14
14
  <link href="/admin/_app/immutable/chunks/B2bEC_Hm.js" rel="modulepreload">
15
15
  <link href="/admin/_app/immutable/chunks/Bb0e0sAP.js" rel="modulepreload">
16
16
  <link href="/admin/_app/immutable/chunks/DtZk82gG.js" rel="modulepreload">
@@ -26,7 +26,7 @@
26
26
  <div style="display: contents">
27
27
  <script>
28
28
  {
29
- __sveltekit_x4yv2o = {
29
+ __sveltekit_1k0lbsj = {
30
30
  base: "/admin",
31
31
  assets: "/admin"
32
32
  };
@@ -34,8 +34,8 @@
34
34
  const element = document.currentScript.parentElement;
35
35
 
36
36
  Promise.all([
37
- import("/admin/_app/immutable/entry/start.CtsqDyfj.js"),
38
- import("/admin/_app/immutable/entry/app.C8ylfBe6.js")
37
+ import("/admin/_app/immutable/entry/start.Cl6sLxnz.js"),
38
+ import("/admin/_app/immutable/entry/app.D3flihMw.js")
39
39
  ]).then(([kit, app]) => {
40
40
  kit.start(app, element);
41
41
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-base/server",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "EdgeBase runtime assets consumed by the EdgeBase CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  "jose": "^6.0.0",
35
35
  "pg": "^8.16.3",
36
36
  "zod": "^4.3.6",
37
- "@edge-base/core": "0.2.4",
38
- "@edge-base/shared": "0.2.4"
37
+ "@edge-base/core": "0.2.5",
38
+ "@edge-base/shared": "0.2.5"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@cloudflare/vitest-pool-workers": "^0.8.71",
@@ -25,11 +25,12 @@ function createCtx() {
25
25
  } as unknown as DurableObjectState;
26
26
  }
27
27
 
28
- function createEnv() {
28
+ function createEnv(config?: unknown) {
29
29
  return {
30
30
  DATABASE_LIVE: {} as DurableObjectNamespace,
31
31
  DATABASE: {} as DurableObjectNamespace,
32
32
  AUTH: {} as DurableObjectNamespace,
33
+ EDGEBASE_CONFIG: config,
33
34
  };
34
35
  }
35
36
 
@@ -39,7 +40,7 @@ describe('DatabaseDO route validation', () => {
39
40
  });
40
41
 
41
42
  it('rejects id-suffixed doNames for single-instance namespaces', async () => {
42
- setConfig(defineConfig({
43
+ const config = defineConfig({
43
44
  release: true,
44
45
  databases: {
45
46
  app: {
@@ -49,11 +50,12 @@ describe('DatabaseDO route validation', () => {
49
50
  },
50
51
  },
51
52
  },
52
- }));
53
+ });
54
+ setConfig(config);
53
55
 
54
56
  const { DatabaseDO } = await import('../durable-objects/database-do.js');
55
57
  const ctx = createCtx();
56
- const databaseDo = new DatabaseDO(ctx, createEnv() as never);
58
+ const databaseDo = new DatabaseDO(ctx, createEnv(config) as never);
57
59
 
58
60
  const response = await databaseDo.fetch(new Request('http://do/tables/posts', {
59
61
  headers: {
@@ -71,7 +73,7 @@ describe('DatabaseDO route validation', () => {
71
73
  });
72
74
 
73
75
  it('rejects missing instance ids for dynamic namespaces', async () => {
74
- setConfig(defineConfig({
76
+ const config = defineConfig({
75
77
  release: true,
76
78
  databases: {
77
79
  workspace: {
@@ -82,11 +84,12 @@ describe('DatabaseDO route validation', () => {
82
84
  },
83
85
  },
84
86
  },
85
- }));
87
+ });
88
+ setConfig(config);
86
89
 
87
90
  const { DatabaseDO } = await import('../durable-objects/database-do.js');
88
91
  const ctx = createCtx();
89
- const databaseDo = new DatabaseDO(ctx, createEnv() as never);
92
+ const databaseDo = new DatabaseDO(ctx, createEnv(config) as never);
90
93
 
91
94
  const response = await databaseDo.fetch(new Request('http://do/tables/users', {
92
95
  headers: {
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Meta-test: route registration completeness.
3
3
  *
4
- * Ensures every route file imported in index.ts is also registered via app.route().
5
- * The expected route exports are derived from the current route files.
4
+ * Ensures every route module imported in index.ts is also registered via app.route().
5
+ * The expected route exports are derived from the route modules the entrypoint actually loads.
6
6
  */
7
- import { readFileSync, readdirSync } from 'fs';
7
+ import { readFileSync } from 'fs';
8
8
  import { resolve } from 'path';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { describe, it, expect } from 'vitest';
@@ -15,9 +15,11 @@ describe('index.ts route registration completeness', () => {
15
15
  'utf-8',
16
16
  );
17
17
  const routesDir = resolve(fileURLToPath(new URL('../routes', import.meta.url)));
18
- const EXPECTED_ROUTES = readdirSync(routesDir)
19
- .filter((fileName) => fileName.endsWith('.ts'))
20
- .sort()
18
+ const ROUTE_IMPORTS = [...source.matchAll(/import\('\.\/routes\/([^']+\.js)'\)/g)]
19
+ .map((match) => match[1])
20
+ .sort();
21
+ const ROUTE_FILES = ROUTE_IMPORTS.map((fileName) => fileName.replace(/\.js$/, '.ts'));
22
+ const EXPECTED_ROUTES = ROUTE_FILES
21
23
  .flatMap((fileName) => {
22
24
  const routeSource = readFileSync(resolve(routesDir, fileName), 'utf-8');
23
25
  const directExports = [...routeSource.matchAll(/export const (\w+)\s*=\s*new OpenAPIHono/g)].map(
@@ -29,20 +31,23 @@ describe('index.ts route registration completeness', () => {
29
31
  return [...directExports, ...aliasExports];
30
32
  });
31
33
 
32
- const EXPECTED_COUNT = EXPECTED_ROUTES.length;
33
-
34
- it(`total route imports = ${EXPECTED_COUNT}`, () => {
35
- const importMatches = source.match(/import \{ \w+ \} from '\.\/routes\//g) || [];
36
- expect(importMatches.length).toBe(EXPECTED_COUNT);
34
+ it(`total route module imports = ${ROUTE_FILES.length}`, () => {
35
+ expect(ROUTE_IMPORTS.length).toBe(ROUTE_FILES.length);
37
36
  });
38
37
 
39
- for (const routeVar of EXPECTED_ROUTES) {
40
- it(`${routeVar} is imported`, () => {
41
- expect(source).toMatch(new RegExp(`import\\s*\\{[^}]*\\b${routeVar}\\b[^}]*\\}\\s*from '\\./routes/`));
38
+ for (const routeFile of ROUTE_FILES) {
39
+ const routePath = routeFile.replace(/\.ts$/, '.js');
40
+
41
+ it(`${routeFile} is dynamically imported`, () => {
42
+ expect(source).toContain(`import('./routes/${routePath}')`);
42
43
  });
44
+ }
43
45
 
46
+ for (const routeVar of EXPECTED_ROUTES) {
44
47
  it(`${routeVar} is registered via app.route()`, () => {
45
- expect(source).toMatch(new RegExp(`app\\.route\\([^)]+,\\s*${routeVar}\\)`));
48
+ const directRegistration = new RegExp(`app\\.route\\([^)]+,\\s*${routeVar}\\)`);
49
+ const moduleRegistration = new RegExp(`app\\.route\\([^)]+,\\s*\\w+\\.${routeVar}\\)`);
50
+ expect(directRegistration.test(source) || moduleRegistration.test(source)).toBe(true);
46
51
  });
47
52
  }
48
53
  });
@@ -5,6 +5,128 @@ vi.mock('cloudflare:workers', () => ({
5
5
  }));
6
6
 
7
7
  describe('room auth-state loss recovery', () => {
8
+ it('treats ephemeral timer persistence failures as non-fatal', async () => {
9
+ const { RoomRuntimeBaseDO } = await import('../durable-objects/room-runtime-base.js');
10
+
11
+ const room: any = Object.create(RoomRuntimeBaseDO.prototype);
12
+ const pending: Promise<unknown>[] = [];
13
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
14
+
15
+ room.pendingAuth = new Map([['conn-1', Date.now() + 5_000]]);
16
+ room.disconnectTimers = new Map();
17
+ room.namespace = 'game';
18
+ room.roomId = 'room-1';
19
+ room.ctx = {
20
+ storage: {
21
+ put: vi.fn().mockRejectedValue(new Error('Exceeded allowed rows written in Durable Objects free tier.')),
22
+ delete: vi.fn(),
23
+ },
24
+ waitUntil: vi.fn((promise: Promise<unknown>) => {
25
+ pending.push(promise);
26
+ }),
27
+ };
28
+
29
+ expect(() => room.syncEphemeralTimersToStorage()).not.toThrow();
30
+ await Promise.allSettled(pending);
31
+
32
+ expect(warnSpy).toHaveBeenCalledWith(
33
+ '[Room] Ephemeral timer persistence skipped',
34
+ expect.objectContaining({
35
+ room: 'game::room-1',
36
+ pendingAuthCount: 1,
37
+ disconnectCount: 0,
38
+ message: 'Exceeded allowed rows written in Durable Objects free tier.',
39
+ }),
40
+ );
41
+
42
+ warnSpy.mockRestore();
43
+ });
44
+
45
+ it('persists alarm-backed room deadlines alongside auth and disconnect timers', async () => {
46
+ const { RoomRuntimeBaseDO } = await import('../durable-objects/room-runtime-base.js');
47
+
48
+ const pending: Promise<unknown>[] = [];
49
+ const putSpy = vi.fn().mockResolvedValue(undefined);
50
+ const room: any = Object.create(RoomRuntimeBaseDO.prototype);
51
+ room.pendingAuth = new Map([['conn-1', 11_111]]);
52
+ room.disconnectTimers = new Map([['user-1', { fireAt: 22_222, connectionId: 'conn-1' }]]);
53
+ room._stateSaveAt = 33_333;
54
+ room._emptyRoomCleanupAt = 44_444;
55
+ room._stateTTLAlarmAt = 55_555;
56
+ room.ctx = {
57
+ storage: {
58
+ put: putSpy,
59
+ delete: vi.fn(),
60
+ },
61
+ waitUntil: vi.fn((promise: Promise<unknown>) => {
62
+ pending.push(promise);
63
+ }),
64
+ };
65
+
66
+ room.syncEphemeralTimersToStorage();
67
+ await Promise.allSettled(pending);
68
+
69
+ expect(putSpy).toHaveBeenCalledWith('roomEphemeralTimers', {
70
+ pendingAuth: { 'conn-1': 11_111 },
71
+ disconnects: { 'user-1': { fireAt: 22_222, connectionId: 'conn-1' } },
72
+ stateSaveAt: 33_333,
73
+ emptyRoomCleanupAt: 44_444,
74
+ stateTTLAlarmAt: 55_555,
75
+ });
76
+ });
77
+
78
+ it('does not rewrite ephemeral timer storage when state is already dirty', async () => {
79
+ const { RoomRuntimeBaseDO } = await import('../durable-objects/room-runtime-base.js');
80
+
81
+ const room: any = Object.create(RoomRuntimeBaseDO.prototype);
82
+ room.dirty = false;
83
+ room._stateSaveAt = 33_333;
84
+ room.namespaceConfig = {};
85
+ room.syncEphemeralTimersToStorage = vi.fn();
86
+ room._scheduleNextAlarm = vi.fn();
87
+
88
+ room.markDirty();
89
+
90
+ expect(room.dirty).toBe(true);
91
+ expect(room._stateSaveAt).toBe(33_333);
92
+ expect(room.syncEphemeralTimersToStorage).not.toHaveBeenCalled();
93
+ expect(room._scheduleNextAlarm).toHaveBeenCalledTimes(1);
94
+ });
95
+
96
+ it('recovers persisted timers before alarm processing after a cold wake without sockets', async () => {
97
+ const { RoomRuntimeBaseDO } = await import('../durable-objects/room-runtime-base.js');
98
+
99
+ const room: any = Object.create(RoomRuntimeBaseDO.prototype);
100
+ room.stateRecoveryNeeded = false;
101
+ room.roomCreated = false;
102
+ room.sharedState = {};
103
+ room.playerStates = new Map();
104
+ room.serverState = {};
105
+ room.players = new Map();
106
+ room.userToConnections = new Map();
107
+ room.pendingAuth = new Map();
108
+ room.disconnectTimers = new Map();
109
+ room._timers = new Map();
110
+ room._stateSaveAt = null;
111
+ room._emptyRoomCleanupAt = null;
112
+ room._stateTTLAlarmAt = null;
113
+ room._metadata = {};
114
+ room.config = {};
115
+ room.ctx = {
116
+ getWebSockets: vi.fn(() => []),
117
+ };
118
+ room.ensureRuntimeReady = vi.fn(async () => {});
119
+ room.recoverFromStorage = vi.fn(async () => {});
120
+ room.findWebSocketByConnectionId = vi.fn(() => null);
121
+ room.finalizePlayerLeave = vi.fn(async () => {});
122
+ room.syncEphemeralTimersToStorage = vi.fn();
123
+ room._scheduleNextAlarm = vi.fn();
124
+
125
+ await room.alarm();
126
+
127
+ expect(room.recoverFromStorage).toHaveBeenCalledTimes(1);
128
+ });
129
+
8
130
  it('marks websocket metadata rebuilt from hibernation tags as auth-state-lost', async () => {
9
131
  const { RoomRuntimeBaseDO } = await import('../durable-objects/room-runtime-base.js');
10
132
 
@@ -48,7 +48,7 @@ describe('RoomsDO handler context', () => {
48
48
  },
49
49
  };
50
50
 
51
- const ctx = room.buildHandlerContext();
51
+ const ctx = await room.buildHandlerContext();
52
52
  const inserted = await ctx.admin.db('shared').table('signals').insert({ title: 'Room inserted' });
53
53
 
54
54
  expect(inserted).toEqual({ id: 'sig-1', title: 'Room inserted' });
@@ -63,7 +63,7 @@ describe('RoomsDO handler context', () => {
63
63
  }),
64
64
  }),
65
65
  );
66
- });
66
+ }, 15_000);
67
67
 
68
68
  it('routes admin.db().upsert() through the database durable object', async () => {
69
69
  const { RoomsDO } = await import('../durable-objects/rooms-do.js');
@@ -104,7 +104,7 @@ describe('RoomsDO handler context', () => {
104
104
  },
105
105
  };
106
106
 
107
- const ctx = room.buildHandlerContext();
107
+ const ctx = await room.buildHandlerContext();
108
108
  const upserted = await ctx.admin.db('shared').table('signals').upsert({
109
109
  id: 'sig-1',
110
110
  title: 'Room upserted',
@@ -126,7 +126,7 @@ describe('RoomsDO handler context', () => {
126
126
  }),
127
127
  }),
128
128
  );
129
- });
129
+ }, 15_000);
130
130
 
131
131
  it('returns 409 when creating a Cloudflare RealtimeKit session while media is already published', async () => {
132
132
  const { RoomsDO } = await import('../durable-objects/rooms-do.js');
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('cloudflare:workers', () => ({
4
+ DurableObject: class DurableObject {},
5
+ }));
6
+
7
+ describe('room rate-limit scopes', () => {
8
+ it('keeps signal/media/admin buckets independent per connection', async () => {
9
+ const { RoomRuntimeBaseDO } = await import('../durable-objects/room-runtime-base.js');
10
+
11
+ const room: any = Object.create(RoomRuntimeBaseDO.prototype);
12
+ room.namespaceConfig = {
13
+ rateLimit: {
14
+ actions: 2,
15
+ signals: 4,
16
+ media: 1,
17
+ admin: 1,
18
+ },
19
+ };
20
+ room.rateBuckets = new Map();
21
+
22
+ expect(room.checkRateLimit('conn-1', 'signals')).toBe(true);
23
+ expect(room.checkRateLimit('conn-1', 'signals')).toBe(true);
24
+ expect(room.checkRateLimit('conn-1', 'signals')).toBe(true);
25
+ expect(room.checkRateLimit('conn-1', 'signals')).toBe(true);
26
+ expect(room.checkRateLimit('conn-1', 'signals')).toBe(false);
27
+
28
+ expect(room.checkRateLimit('conn-1', 'media')).toBe(true);
29
+ expect(room.checkRateLimit('conn-1', 'media')).toBe(false);
30
+
31
+ expect(room.checkRateLimit('conn-1', 'admin')).toBe(true);
32
+ expect(room.checkRateLimit('conn-1', 'admin')).toBe(false);
33
+
34
+ expect(room.checkRateLimit('conn-1', 'actions')).toBe(true);
35
+ expect(room.checkRateLimit('conn-1', 'actions')).toBe(true);
36
+ expect(room.checkRateLimit('conn-1', 'actions')).toBe(false);
37
+ });
38
+ });
@@ -0,0 +1,49 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('cloudflare:workers', () => ({
4
+ DurableObject: class DurableObject {},
5
+ }));
6
+
7
+ afterEach(() => {
8
+ vi.resetModules();
9
+ if (typeof globalThis === 'object' && globalThis !== null) {
10
+ delete (globalThis as Record<string, unknown>).__EDGEBASE_RUNTIME_CONFIG__;
11
+ }
12
+ });
13
+
14
+ describe('runtime startup bootstrap', () => {
15
+ it('initializes runtime config idempotently for lazy server and DO entrypoints', async () => {
16
+ const { ensureServerStartup } = await import('../lib/runtime-startup.js');
17
+ const { parseConfig } = await import('../lib/do-router.js');
18
+
19
+ await expect(ensureServerStartup()).resolves.toBeUndefined();
20
+ const firstConfig = parseConfig();
21
+
22
+ await expect(ensureServerStartup()).resolves.toBeUndefined();
23
+ const secondConfig = parseConfig();
24
+
25
+ expect(firstConfig).toEqual(secondConfig);
26
+ expect(secondConfig).toBeTypeOf('object');
27
+ });
28
+
29
+ it('does not clobber an explicitly injected runtime config', async () => {
30
+ const { ensureServerStartup } = await import('../lib/runtime-startup.js');
31
+ const { parseConfig, setConfig } = await import('../lib/do-router.js');
32
+
33
+ setConfig({
34
+ release: false,
35
+ auth: {
36
+ allowedRedirectUrls: ['http://localhost:4173'],
37
+ },
38
+ });
39
+
40
+ await expect(ensureServerStartup()).resolves.toBeUndefined();
41
+
42
+ expect(parseConfig()).toMatchObject({
43
+ release: false,
44
+ auth: {
45
+ allowedRedirectUrls: ['http://localhost:4173'],
46
+ },
47
+ });
48
+ });
49
+ });
@@ -60,6 +60,7 @@ import { buildDbLiveChannel, DATABASE_LIVE_HUB_DO_NAME } from '../lib/database-l
60
60
  import { resolveRootServiceKey } from '../lib/service-key.js';
61
61
  import { resolveDbLiveBatchThreshold } from '../lib/database-live-config.js';
62
62
  import { buildTableHookRuntimeServices } from '../lib/table-hook-runtime.js';
63
+ import { ensureServerStartup } from '../lib/runtime-startup.js';
63
64
  import type { Env } from '../types.js';
64
65
 
65
66
  // ─── Types ───
@@ -80,6 +81,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
80
81
  private config: EdgeBaseConfig;
81
82
  private initialized = false;
82
83
  private doName = '';
84
+ private runtimeReadyPromise: Promise<void> | null = null;
83
85
 
84
86
  constructor(ctx: DurableObjectState, env: DOEnv) {
85
87
  super(ctx, env);
@@ -92,6 +94,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
92
94
  }
93
95
 
94
96
  async fetch(request: Request): Promise<Response> {
97
+ await this.ensureRuntimeReady();
95
98
  // Determine DO name from header or URL
96
99
  const doNameHeader = request.headers.get('X-DO-Name');
97
100
 
@@ -1991,6 +1994,17 @@ export class DatabaseDO extends DurableObject<DOEnv> {
1991
1994
  return getGlobalConfig(env);
1992
1995
  }
1993
1996
 
1997
+ private async ensureRuntimeReady(): Promise<void> {
1998
+ if (!this.runtimeReadyPromise) {
1999
+ this.runtimeReadyPromise = (async () => {
2000
+ await ensureServerStartup();
2001
+ this.config = this.parseConfig(this.env);
2002
+ })();
2003
+ }
2004
+
2005
+ await this.runtimeReadyPromise;
2006
+ }
2007
+
1994
2008
  // ─── Database Live Event Emission ───
1995
2009
 
1996
2010
  /**
@@ -7,6 +7,7 @@ import { verifyAccessToken } from '../lib/jwt.js';
7
7
  import { parseConfig as getGlobalConfig } from '../lib/do-router.js';
8
8
  import { isDbLiveChannel } from '../lib/database-live-emitter.js';
9
9
  import { resolveDbLiveAuthTimeoutMs } from '../lib/database-live-config.js';
10
+ import { ensureServerStartup } from '../lib/runtime-startup.js';
10
11
 
11
12
  interface DOEnv {
12
13
  JWT_USER_SECRET?: string;
@@ -142,6 +143,7 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
142
143
  private pendingAuth = new Map<string, ReturnType<typeof setTimeout>>();
143
144
  private metaCache = new Map<WebSocket, WSMeta>();
144
145
  private recentDeliveryIds = new Map<string, number>();
146
+ private runtimeReadyPromise: Promise<void> | null = null;
145
147
 
146
148
  constructor(ctx: DurableObjectState, env: DOEnv) {
147
149
  super(ctx, env);
@@ -149,6 +151,7 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
149
151
  }
150
152
 
151
153
  async fetch(request: Request): Promise<Response> {
154
+ await this.ensureRuntimeReady();
152
155
  const url = new URL(request.url);
153
156
 
154
157
  if (url.pathname === '/internal/event') {
@@ -218,6 +221,7 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
218
221
  }
219
222
 
220
223
  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
224
+ await this.ensureRuntimeReady();
221
225
  if (typeof message !== 'string') return;
222
226
 
223
227
  let msg: Record<string, unknown>;
@@ -922,6 +926,17 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
922
926
  if (parts.length >= 5) return parts[3];
923
927
  return null;
924
928
  }
929
+
930
+ private async ensureRuntimeReady(): Promise<void> {
931
+ if (!this.runtimeReadyPromise) {
932
+ this.runtimeReadyPromise = (async () => {
933
+ await ensureServerStartup();
934
+ this.config = getGlobalConfig(this.env);
935
+ })();
936
+ }
937
+
938
+ await this.runtimeReadyPromise;
939
+ }
925
940
  }
926
941
 
927
942
  export function evaluateDatabaseLiveFilters(