@edge-base/server 0.2.2 → 0.2.4
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.
- package/admin-build/_app/immutable/chunks/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
- package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
- package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
- package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
- package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
- package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
- package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
- package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
- package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-data-routes.test.ts +29 -0
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-do-route-validation.test.ts +105 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +674 -33
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +163 -0
- package/src/__tests__/room-auth-state-loss.test.ts +124 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
- package/src/__tests__/scheduled.test.ts +55 -0
- package/src/__tests__/service-key-db-proxy.test.ts +122 -1
- package/src/__tests__/sql-route.test.ts +252 -75
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +36 -45
- package/src/durable-objects/database-live-do.ts +46 -1
- package/src/durable-objects/room-runtime-base.ts +26 -2
- package/src/durable-objects/rooms-do.ts +1 -1
- package/src/index.ts +12 -6
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +55 -35
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +215 -143
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/plugin-migrations.ts +38 -38
- package/src/lib/postgres-handler.ts +51 -31
- package/src/lib/provider-aware-sql.ts +831 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/auth.ts +7 -2
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +64 -84
- package/src/routes/storage.ts +7 -2
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
- package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
- package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
- package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.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/
|
|
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 +1 @@
|
|
|
1
|
-
{"version":"
|
|
1
|
+
{"version":"1774395752593"}
|
package/admin-build/index.html
CHANGED
|
@@ -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.
|
|
9
|
-
<link href="/admin/_app/immutable/chunks/
|
|
8
|
+
<link href="/admin/_app/immutable/entry/start.CtsqDyfj.js" rel="modulepreload">
|
|
9
|
+
<link href="/admin/_app/immutable/chunks/D__dwMuW.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/
|
|
13
|
-
<link href="/admin/_app/immutable/entry/app.
|
|
12
|
+
<link href="/admin/_app/immutable/chunks/DBsVqhuh.js" rel="modulepreload">
|
|
13
|
+
<link href="/admin/_app/immutable/entry/app.C8ylfBe6.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
|
-
|
|
29
|
+
__sveltekit_x4yv2o = {
|
|
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.
|
|
38
|
-
import("/admin/_app/immutable/entry/app.
|
|
37
|
+
import("/admin/_app/immutable/entry/start.CtsqDyfj.js"),
|
|
38
|
+
import("/admin/_app/immutable/entry/app.C8ylfBe6.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.
|
|
3
|
+
"version": "0.2.4",
|
|
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/
|
|
38
|
-
"@edge-base/
|
|
37
|
+
"@edge-base/core": "0.2.4",
|
|
38
|
+
"@edge-base/shared": "0.2.4"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@cloudflare/vitest-pool-workers": "^0.8.71",
|
|
@@ -165,6 +165,35 @@ describe('admin data routes', () => {
|
|
|
165
165
|
});
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
it('rejects instanceId query params for single-instance record browsing', async () => {
|
|
169
|
+
setConfig(
|
|
170
|
+
createConfig({
|
|
171
|
+
shared: {
|
|
172
|
+
tables: {
|
|
173
|
+
posts: { schema: { title: { type: 'string' } } },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const app = createApp();
|
|
180
|
+
const response = await app.request(
|
|
181
|
+
'/admin/api/data/tables/posts/records?instanceId=shadow',
|
|
182
|
+
{
|
|
183
|
+
headers: {
|
|
184
|
+
'X-EdgeBase-Service-Key': 'sk-root',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{} as Env,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(response.status).toBe(400);
|
|
191
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
192
|
+
code: 400,
|
|
193
|
+
message: "instanceId is not allowed for single-instance namespace 'shared'",
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
168
197
|
it('routes dynamic record browsing to the scoped DO when instanceId is provided', async () => {
|
|
169
198
|
setConfig(
|
|
170
199
|
createConfig({
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { defineConfig } from '@edge-base/shared';
|
|
3
|
+
import { buildInternalHandlerContext } from '../lib/internal-request.js';
|
|
4
|
+
import type { Env } from '../types.js';
|
|
5
|
+
|
|
6
|
+
function createInsertMockD1(row: Record<string, unknown>): D1Database {
|
|
7
|
+
return {
|
|
8
|
+
prepare(sql: string) {
|
|
9
|
+
const state = { bindings: [] as unknown[] };
|
|
10
|
+
const stmt = {
|
|
11
|
+
bind: (...values: unknown[]) => {
|
|
12
|
+
state.bindings = values;
|
|
13
|
+
return stmt;
|
|
14
|
+
},
|
|
15
|
+
all: async () => {
|
|
16
|
+
if (sql.startsWith('INSERT INTO')) {
|
|
17
|
+
return { results: [], meta: { changes: 1 } };
|
|
18
|
+
}
|
|
19
|
+
if (sql.startsWith('SELECT * FROM')) {
|
|
20
|
+
return { results: [row], meta: { changes: 0 } };
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Unexpected SQL: ${sql}`);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
return stmt;
|
|
26
|
+
},
|
|
27
|
+
} as unknown as D1Database;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createBatchInsertMockD1(rowsById: Record<string, Record<string, unknown>>): D1Database {
|
|
31
|
+
return {
|
|
32
|
+
prepare(sql: string) {
|
|
33
|
+
const state = { bindings: [] as unknown[] };
|
|
34
|
+
const stmt = {
|
|
35
|
+
_sql: sql,
|
|
36
|
+
bind: (...values: unknown[]) => {
|
|
37
|
+
state.bindings = values;
|
|
38
|
+
return stmt;
|
|
39
|
+
},
|
|
40
|
+
all: async () => {
|
|
41
|
+
if (!sql.startsWith('SELECT * FROM')) {
|
|
42
|
+
throw new Error(`Unexpected SQL: ${sql}`);
|
|
43
|
+
}
|
|
44
|
+
const id = String(state.bindings[0]);
|
|
45
|
+
return {
|
|
46
|
+
results: rowsById[id] ? [rowsById[id]] : [],
|
|
47
|
+
meta: { changes: 0 },
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return stmt;
|
|
52
|
+
},
|
|
53
|
+
batch: async () => [],
|
|
54
|
+
} as unknown as D1Database;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createEnv(db: D1Database): Env {
|
|
58
|
+
return {
|
|
59
|
+
EDGEBASE_CONFIG: defineConfig({
|
|
60
|
+
release: true,
|
|
61
|
+
databases: {
|
|
62
|
+
shared: {
|
|
63
|
+
tables: {
|
|
64
|
+
posts: {
|
|
65
|
+
schema: {
|
|
66
|
+
title: { type: 'string', required: true },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
DB_D1_SHARED: db,
|
|
74
|
+
DATABASE: {} as DurableObjectNamespace,
|
|
75
|
+
AUTH: {} as DurableObjectNamespace,
|
|
76
|
+
KV: {} as KVNamespace,
|
|
77
|
+
} as unknown as Env;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('d1 live broadcast verification', () => {
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.resetModules();
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
vi.unstubAllGlobals();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns single-record insert responses without waiting for emitDbLiveEvent', async () => {
|
|
88
|
+
let resolveEmit!: () => void;
|
|
89
|
+
const emitGate = new Promise<void>((resolve) => {
|
|
90
|
+
resolveEmit = resolve;
|
|
91
|
+
});
|
|
92
|
+
const emitDbLiveEvent = vi.fn(() => emitGate);
|
|
93
|
+
const emitDbLiveBatchEvent = vi.fn().mockResolvedValue(undefined);
|
|
94
|
+
const sendToDatabaseLiveDO = vi.fn().mockResolvedValue(undefined);
|
|
95
|
+
const executeDbTriggers = vi.fn().mockResolvedValue(undefined);
|
|
96
|
+
|
|
97
|
+
vi.doMock('../lib/d1-schema-init.js', () => ({
|
|
98
|
+
ensureD1Schema: vi.fn().mockResolvedValue(undefined),
|
|
99
|
+
}));
|
|
100
|
+
vi.doMock('../lib/database-live-emitter.js', () => ({
|
|
101
|
+
emitDbLiveEvent,
|
|
102
|
+
emitDbLiveBatchEvent,
|
|
103
|
+
sendToDatabaseLiveDO,
|
|
104
|
+
}));
|
|
105
|
+
vi.doMock('../lib/functions.js', () => ({
|
|
106
|
+
executeDbTriggers,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const { handleD1Request } = await import('../lib/d1-handler.js');
|
|
110
|
+
|
|
111
|
+
const env = createEnv(createInsertMockD1({ id: 'post-1', title: 'hello world' }));
|
|
112
|
+
const waitUntil = vi.fn();
|
|
113
|
+
const ctx = buildInternalHandlerContext({
|
|
114
|
+
env,
|
|
115
|
+
executionCtx: { waitUntil } as unknown as ExecutionContext,
|
|
116
|
+
request: new Request('http://internal/api/db/shared/tables/posts', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
'X-Is-Service-Key': 'true',
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
body: {
|
|
124
|
+
id: 'post-1',
|
|
125
|
+
title: 'hello world',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let settled = false;
|
|
130
|
+
const responsePromise = handleD1Request(ctx, 'shared', 'posts', '/tables/posts').then((response) => {
|
|
131
|
+
settled = true;
|
|
132
|
+
return response;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await vi.waitFor(() => {
|
|
136
|
+
expect(emitDbLiveEvent).toHaveBeenCalledTimes(1);
|
|
137
|
+
});
|
|
138
|
+
await vi.waitFor(() => {
|
|
139
|
+
expect(settled).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const response = await responsePromise;
|
|
143
|
+
const json = await response.json() as Record<string, unknown>;
|
|
144
|
+
|
|
145
|
+
expect(response.status).toBe(201);
|
|
146
|
+
expect(json).toMatchObject({ id: 'post-1', title: 'hello world' });
|
|
147
|
+
expect(waitUntil).toHaveBeenCalled();
|
|
148
|
+
|
|
149
|
+
resolveEmit();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('sends small batches through a single background Promise.all instead of awaiting fan-out', async () => {
|
|
153
|
+
let resolveFanOut!: () => void;
|
|
154
|
+
const fanOutGate = new Promise<void>((resolve) => {
|
|
155
|
+
resolveFanOut = resolve;
|
|
156
|
+
});
|
|
157
|
+
const emitDbLiveEvent = vi.fn(() => fanOutGate);
|
|
158
|
+
const emitDbLiveBatchEvent = vi.fn().mockResolvedValue(undefined);
|
|
159
|
+
const sendToDatabaseLiveDO = vi.fn().mockResolvedValue(undefined);
|
|
160
|
+
|
|
161
|
+
vi.doMock('../lib/d1-schema-init.js', () => ({
|
|
162
|
+
ensureD1Schema: vi.fn().mockResolvedValue(undefined),
|
|
163
|
+
}));
|
|
164
|
+
vi.doMock('../lib/database-live-emitter.js', () => ({
|
|
165
|
+
emitDbLiveEvent,
|
|
166
|
+
emitDbLiveBatchEvent,
|
|
167
|
+
sendToDatabaseLiveDO,
|
|
168
|
+
}));
|
|
169
|
+
vi.doMock('../lib/functions.js', () => ({
|
|
170
|
+
executeDbTriggers: vi.fn().mockResolvedValue(undefined),
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
const { handleD1Request } = await import('../lib/d1-handler.js');
|
|
174
|
+
|
|
175
|
+
const rowsById = {
|
|
176
|
+
'post-1': { id: 'post-1', title: 'post 1' },
|
|
177
|
+
'post-2': { id: 'post-2', title: 'post 2' },
|
|
178
|
+
};
|
|
179
|
+
const env = createEnv(createBatchInsertMockD1(rowsById));
|
|
180
|
+
const waitUntil = vi.fn();
|
|
181
|
+
const ctx = buildInternalHandlerContext({
|
|
182
|
+
env,
|
|
183
|
+
executionCtx: { waitUntil } as unknown as ExecutionContext,
|
|
184
|
+
request: new Request('http://internal/api/db/shared/tables/posts/batch', {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: {
|
|
187
|
+
'Content-Type': 'application/json',
|
|
188
|
+
'X-Is-Service-Key': 'true',
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
body: {
|
|
192
|
+
inserts: Object.values(rowsById),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
let settled = false;
|
|
197
|
+
const responsePromise = handleD1Request(ctx, 'shared', 'posts', '/tables/posts/batch').then((response) => {
|
|
198
|
+
settled = true;
|
|
199
|
+
return response;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await vi.waitFor(() => {
|
|
203
|
+
expect(emitDbLiveEvent).toHaveBeenCalledTimes(2);
|
|
204
|
+
});
|
|
205
|
+
await vi.waitFor(() => {
|
|
206
|
+
expect(settled).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const response = await responsePromise;
|
|
210
|
+
const json = await response.json() as { inserted: Array<Record<string, unknown>> };
|
|
211
|
+
|
|
212
|
+
expect(response.status).toBe(200);
|
|
213
|
+
expect(json.inserted).toHaveLength(2);
|
|
214
|
+
expect(emitDbLiveBatchEvent).not.toHaveBeenCalled();
|
|
215
|
+
expect(waitUntil).toHaveBeenCalledTimes(1);
|
|
216
|
+
|
|
217
|
+
resolveFanOut();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('still sends 10+ change batches through waitUntil instead of awaiting the batch emitter', async () => {
|
|
221
|
+
const emitDbLiveEvent = vi.fn().mockResolvedValue(undefined);
|
|
222
|
+
const emitDbLiveBatchEvent = vi.fn(() => new Promise<void>(() => {}));
|
|
223
|
+
const sendToDatabaseLiveDO = vi.fn().mockResolvedValue(undefined);
|
|
224
|
+
|
|
225
|
+
vi.doMock('../lib/d1-schema-init.js', () => ({
|
|
226
|
+
ensureD1Schema: vi.fn().mockResolvedValue(undefined),
|
|
227
|
+
}));
|
|
228
|
+
vi.doMock('../lib/database-live-emitter.js', () => ({
|
|
229
|
+
emitDbLiveEvent,
|
|
230
|
+
emitDbLiveBatchEvent,
|
|
231
|
+
sendToDatabaseLiveDO,
|
|
232
|
+
}));
|
|
233
|
+
vi.doMock('../lib/functions.js', () => ({
|
|
234
|
+
executeDbTriggers: vi.fn().mockResolvedValue(undefined),
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
const { handleD1Request } = await import('../lib/d1-handler.js');
|
|
238
|
+
|
|
239
|
+
const rowsById = Object.fromEntries(
|
|
240
|
+
Array.from({ length: 10 }, (_value, index) => {
|
|
241
|
+
const id = `post-${index + 1}`;
|
|
242
|
+
return [id, { id, title: `post ${index + 1}` }];
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
const env = createEnv(createBatchInsertMockD1(rowsById));
|
|
246
|
+
const waitUntil = vi.fn();
|
|
247
|
+
const ctx = buildInternalHandlerContext({
|
|
248
|
+
env,
|
|
249
|
+
executionCtx: { waitUntil } as unknown as ExecutionContext,
|
|
250
|
+
request: new Request('http://internal/api/db/shared/tables/posts/batch', {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: {
|
|
253
|
+
'Content-Type': 'application/json',
|
|
254
|
+
'X-Is-Service-Key': 'true',
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
body: {
|
|
258
|
+
inserts: Object.values(rowsById),
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const response = await handleD1Request(ctx, 'shared', 'posts', '/tables/posts/batch');
|
|
263
|
+
const json = await response.json() as { inserted: Array<Record<string, unknown>> };
|
|
264
|
+
|
|
265
|
+
expect(response.status).toBe(200);
|
|
266
|
+
expect(json.inserted).toHaveLength(10);
|
|
267
|
+
expect(emitDbLiveBatchEvent).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(emitDbLiveEvent).not.toHaveBeenCalled();
|
|
269
|
+
expect(waitUntil).toHaveBeenCalledTimes(1);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { defineConfig } from '@edge-base/shared';
|
|
3
|
+
import { setConfig } from '../lib/do-router.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('cloudflare:workers', () => ({
|
|
6
|
+
DurableObject: class DurableObject {
|
|
7
|
+
ctx: unknown;
|
|
8
|
+
env: unknown;
|
|
9
|
+
|
|
10
|
+
constructor(ctx: unknown, env: unknown) {
|
|
11
|
+
this.ctx = ctx;
|
|
12
|
+
this.env = env;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function createCtx() {
|
|
18
|
+
return {
|
|
19
|
+
storage: {
|
|
20
|
+
sql: {
|
|
21
|
+
exec: vi.fn(),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
waitUntil: vi.fn(),
|
|
25
|
+
} as unknown as DurableObjectState;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createEnv() {
|
|
29
|
+
return {
|
|
30
|
+
DATABASE_LIVE: {} as DurableObjectNamespace,
|
|
31
|
+
DATABASE: {} as DurableObjectNamespace,
|
|
32
|
+
AUTH: {} as DurableObjectNamespace,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('DatabaseDO route validation', () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
setConfig({});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects id-suffixed doNames for single-instance namespaces', async () => {
|
|
42
|
+
setConfig(defineConfig({
|
|
43
|
+
release: true,
|
|
44
|
+
databases: {
|
|
45
|
+
app: {
|
|
46
|
+
provider: 'do',
|
|
47
|
+
tables: {
|
|
48
|
+
posts: {},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const { DatabaseDO } = await import('../durable-objects/database-do.js');
|
|
55
|
+
const ctx = createCtx();
|
|
56
|
+
const databaseDo = new DatabaseDO(ctx, createEnv() as never);
|
|
57
|
+
|
|
58
|
+
const response = await databaseDo.fetch(new Request('http://do/tables/posts', {
|
|
59
|
+
headers: {
|
|
60
|
+
'X-DO-Name': 'app:shadow',
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
expect(response.status).toBe(400);
|
|
65
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
66
|
+
code: 400,
|
|
67
|
+
message: "instanceId is not allowed for single-instance namespace 'app'",
|
|
68
|
+
error: 'INVALID_DB_INSTANCE_ID',
|
|
69
|
+
});
|
|
70
|
+
expect((ctx.storage.sql.exec as unknown as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects missing instance ids for dynamic namespaces', async () => {
|
|
74
|
+
setConfig(defineConfig({
|
|
75
|
+
release: true,
|
|
76
|
+
databases: {
|
|
77
|
+
workspace: {
|
|
78
|
+
provider: 'do',
|
|
79
|
+
instance: true,
|
|
80
|
+
tables: {
|
|
81
|
+
users: {},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const { DatabaseDO } = await import('../durable-objects/database-do.js');
|
|
88
|
+
const ctx = createCtx();
|
|
89
|
+
const databaseDo = new DatabaseDO(ctx, createEnv() as never);
|
|
90
|
+
|
|
91
|
+
const response = await databaseDo.fetch(new Request('http://do/tables/users', {
|
|
92
|
+
headers: {
|
|
93
|
+
'X-DO-Name': 'workspace',
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
expect(response.status).toBe(400);
|
|
98
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
99
|
+
code: 400,
|
|
100
|
+
message: "instanceId is required for dynamic namespace 'workspace'",
|
|
101
|
+
error: 'INVALID_DB_INSTANCE_ID',
|
|
102
|
+
});
|
|
103
|
+
expect((ctx.storage.sql.exec as unknown as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('cloudflare:workers', () => ({
|
|
4
|
+
DurableObject: class DurableObject {
|
|
5
|
+
ctx: unknown;
|
|
6
|
+
env: unknown;
|
|
7
|
+
|
|
8
|
+
constructor(ctx: unknown, env: unknown) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
this.env = env;
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('DatabaseLiveDO delivery idempotency', () => {
|
|
16
|
+
it('ignores duplicate internal change events with the same deliveryId', async () => {
|
|
17
|
+
const { DatabaseLiveDO } = await import('../durable-objects/database-live-do.js');
|
|
18
|
+
|
|
19
|
+
const ctx = {
|
|
20
|
+
acceptWebSocket: vi.fn(),
|
|
21
|
+
getWebSockets: vi.fn(() => []),
|
|
22
|
+
getTags: vi.fn(() => []),
|
|
23
|
+
} as any;
|
|
24
|
+
const live = new DatabaseLiveDO(ctx, {} as any) as any;
|
|
25
|
+
live.broadcastWithFilters = vi.fn().mockResolvedValue(undefined);
|
|
26
|
+
|
|
27
|
+
const payload = {
|
|
28
|
+
deliveryId: 'delivery-1',
|
|
29
|
+
type: 'modified',
|
|
30
|
+
channel: 'dblive:shared:posts',
|
|
31
|
+
table: 'posts',
|
|
32
|
+
docId: 'post-1',
|
|
33
|
+
data: { id: 'post-1', title: 'hello' },
|
|
34
|
+
timestamp: '2026-03-24T00:00:00.000Z',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const first = await live.handleInternalEvent(new Request('http://internal/internal/event', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
body: JSON.stringify(payload),
|
|
40
|
+
}));
|
|
41
|
+
const second = await live.handleInternalEvent(new Request('http://internal/internal/event', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: JSON.stringify(payload),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
await expect(first.json()).resolves.toEqual({ ok: true });
|
|
47
|
+
await expect(second.json()).resolves.toEqual({ ok: true, duplicate: true });
|
|
48
|
+
expect(live.broadcastWithFilters).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
buildDbLiveChannel,
|
|
4
|
+
emitDbLiveBatchEvent,
|
|
5
|
+
emitDbLiveEvent,
|
|
6
|
+
isDbLiveChannel,
|
|
7
|
+
sendToDatabaseLiveDO,
|
|
8
|
+
} from '../lib/database-live-emitter.js';
|
|
3
9
|
|
|
4
10
|
describe('buildDbLiveChannel', () => {
|
|
5
11
|
it('builds shared table channels with namespace', () => {
|
|
@@ -13,6 +19,24 @@ describe('buildDbLiveChannel', () => {
|
|
|
13
19
|
});
|
|
14
20
|
});
|
|
15
21
|
|
|
22
|
+
describe('isDbLiveChannel', () => {
|
|
23
|
+
it('accepts valid db-live table and document channels', () => {
|
|
24
|
+
expect(isDbLiveChannel('dblive:shared:posts')).toBe(true);
|
|
25
|
+
expect(isDbLiveChannel('dblive:shared:posts:post-1')).toBe(true);
|
|
26
|
+
expect(isDbLiveChannel('dblive:workspace:ws-1:posts')).toBe(true);
|
|
27
|
+
expect(isDbLiveChannel('dblive:workspace:ws-1:posts:post-1')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects non-db-live, presence, broadcast, malformed, and empty-segment channels', () => {
|
|
31
|
+
expect(isDbLiveChannel('presence:shared:posts')).toBe(false);
|
|
32
|
+
expect(isDbLiveChannel('dblive:presence:posts')).toBe(false);
|
|
33
|
+
expect(isDbLiveChannel('dblive:broadcast:posts')).toBe(false);
|
|
34
|
+
expect(isDbLiveChannel('dblive:shared')).toBe(false);
|
|
35
|
+
expect(isDbLiveChannel('dblive:one:two:three:four:five')).toBe(false);
|
|
36
|
+
expect(isDbLiveChannel('dblive:shared::posts')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
16
40
|
describe('database-live emitter', () => {
|
|
17
41
|
it('emits shared-table events to namespace-aware table and doc channels', async () => {
|
|
18
42
|
const fetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
|
|
@@ -53,4 +77,95 @@ describe('database-live emitter', () => {
|
|
|
53
77
|
const payload = JSON.parse(fetch.mock.calls[0]![1].body as string);
|
|
54
78
|
expect(payload.channel).toBe('dblive:workspace:ws-9:documents');
|
|
55
79
|
});
|
|
80
|
+
|
|
81
|
+
it('rejects when the database-live DO responds with a server error', async () => {
|
|
82
|
+
const fetch = vi.fn().mockResolvedValue(new Response(null, { status: 500 }));
|
|
83
|
+
const env = {
|
|
84
|
+
DATABASE_LIVE: {
|
|
85
|
+
idFromName: vi.fn((name: string) => name),
|
|
86
|
+
get: vi.fn().mockReturnValue({ fetch }),
|
|
87
|
+
},
|
|
88
|
+
} as any;
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
emitDbLiveEvent(env, 'shared', 'posts', 'modified', 'post-1', { id: 'post-1' }),
|
|
92
|
+
).rejects.toThrow(/DatabaseLiveDO .* failed with 500/);
|
|
93
|
+
|
|
94
|
+
expect(fetch).toHaveBeenCalledTimes(4);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('skips the document fan-out for bulk events', async () => {
|
|
98
|
+
const fetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
|
|
99
|
+
const env = {
|
|
100
|
+
DATABASE_LIVE: {
|
|
101
|
+
idFromName: vi.fn((name: string) => name),
|
|
102
|
+
get: vi.fn().mockReturnValue({ fetch }),
|
|
103
|
+
},
|
|
104
|
+
} as any;
|
|
105
|
+
|
|
106
|
+
await emitDbLiveEvent(env, 'shared', 'posts', 'removed', '_bulk', { action: 'delete', count: 3 });
|
|
107
|
+
|
|
108
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
109
|
+
const payload = JSON.parse(fetch.mock.calls[0]![1].body as string);
|
|
110
|
+
expect(payload.channel).toBe('dblive:shared:posts');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns early when DATABASE_LIVE is not configured', async () => {
|
|
114
|
+
await expect(
|
|
115
|
+
sendToDatabaseLiveDO({} as any, { channel: 'dblive:shared:posts', event: 'refresh' }),
|
|
116
|
+
).resolves.toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('reuses the same deliveryId across retries for a single handoff', async () => {
|
|
120
|
+
const fetch = vi.fn()
|
|
121
|
+
.mockResolvedValueOnce(new Response(null, { status: 500 }))
|
|
122
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
123
|
+
const env = {
|
|
124
|
+
DATABASE_LIVE: {
|
|
125
|
+
idFromName: vi.fn((name: string) => name),
|
|
126
|
+
get: vi.fn().mockReturnValue({ fetch }),
|
|
127
|
+
},
|
|
128
|
+
} as any;
|
|
129
|
+
|
|
130
|
+
await sendToDatabaseLiveDO(env, { channel: 'dblive:shared:posts', event: 'refresh' }, '/internal/broadcast');
|
|
131
|
+
|
|
132
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
133
|
+
const firstBody = JSON.parse(fetch.mock.calls[0]![1].body as string);
|
|
134
|
+
const secondBody = JSON.parse(fetch.mock.calls[1]![1].body as string);
|
|
135
|
+
expect(firstBody.deliveryId).toBeTruthy();
|
|
136
|
+
expect(firstBody.deliveryId).toBe(secondBody.deliveryId);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('keeps an existing deliveryId instead of generating a new one', async () => {
|
|
140
|
+
const fetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
|
|
141
|
+
const env = {
|
|
142
|
+
DATABASE_LIVE: {
|
|
143
|
+
idFromName: vi.fn((name: string) => name),
|
|
144
|
+
get: vi.fn().mockReturnValue({ fetch }),
|
|
145
|
+
},
|
|
146
|
+
} as any;
|
|
147
|
+
|
|
148
|
+
await sendToDatabaseLiveDO(env, {
|
|
149
|
+
channel: 'dblive:shared:posts',
|
|
150
|
+
deliveryId: 'delivery-fixed',
|
|
151
|
+
event: 'refresh',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const body = JSON.parse(fetch.mock.calls[0]![1].body as string);
|
|
155
|
+
expect(body.deliveryId).toBe('delivery-fixed');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('throws a generic error when retries fail with a non-Error value', async () => {
|
|
159
|
+
const fetch = vi.fn().mockRejectedValue('boom');
|
|
160
|
+
const env = {
|
|
161
|
+
DATABASE_LIVE: {
|
|
162
|
+
idFromName: vi.fn((name: string) => name),
|
|
163
|
+
get: vi.fn().mockReturnValue({ fetch }),
|
|
164
|
+
},
|
|
165
|
+
} as any;
|
|
166
|
+
|
|
167
|
+
await expect(
|
|
168
|
+
sendToDatabaseLiveDO(env, { channel: 'dblive:shared:posts', event: 'refresh' }),
|
|
169
|
+
).rejects.toThrow('DatabaseLiveDO delivery failed.');
|
|
170
|
+
});
|
|
56
171
|
});
|