@midsummer-ai/sdk 0.0.0-stage → 0.1.0-next.1723
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/LICENSE +177 -0
- package/README.md +4 -2
- package/agents/contract.d.mts +9 -0
- package/agents/contract.mjs +221 -0
- package/agents/index.d.mts +128 -0
- package/agents/index.mjs +203 -0
- package/agents/stand-in.d.mts +35 -0
- package/agents/stand-in.mjs +149 -0
- package/ai/contract.d.mts +10 -0
- package/ai/contract.mjs +77 -0
- package/ai/index.d.mts +94 -0
- package/ai/index.mjs +193 -0
- package/ai/stand-in.d.mts +7 -0
- package/ai/stand-in.mjs +41 -0
- package/build-SQ44jwSa.d.mts +41 -0
- package/client.d.mts +45 -0
- package/client.mjs +106 -0
- package/connection-BHjDaLYA.mjs +2009 -0
- package/contract.d.mts +10 -0
- package/contract.mjs +469 -0
- package/database/build.d.mts +2 -0
- package/database/build.mjs +579 -0
- package/database/client.d.mts +60 -0
- package/database/client.mjs +133 -0
- package/database/contract.d.mts +11 -0
- package/database/contract.mjs +164 -0
- package/database/errors.d.mts +23 -0
- package/database/errors.mjs +13 -0
- package/database/server.d.mts +3 -0
- package/database/server.mjs +320 -0
- package/database/stand-in.d.mts +99 -0
- package/database/stand-in.mjs +430 -0
- package/email/client.d.mts +34 -0
- package/email/client.mjs +131 -0
- package/email/contract.d.mts +11 -0
- package/email/contract.mjs +99 -0
- package/email/index.d.mts +74 -0
- package/email/index.mjs +59 -0
- package/email/smtp.d.mts +22 -0
- package/email/smtp.mjs +296 -0
- package/email/stand-in.d.mts +13 -0
- package/email/stand-in.mjs +51 -0
- package/index-DdoK0y7D.d.mts +192 -0
- package/index-sgdyoA2P.d.mts +212 -0
- package/index.d.mts +2 -0
- package/index.mjs +137 -0
- package/names-DfCBKupJ.mjs +115 -0
- package/package.json +152 -4
- package/predict-Drfm_8pO.mjs +179 -0
- package/queues/contract.d.mts +13 -0
- package/queues/contract.mjs +278 -0
- package/queues/index.d.mts +184 -0
- package/queues/index.mjs +126 -0
- package/queues/stand-in.d.mts +7 -0
- package/queues/stand-in.mjs +110 -0
- package/queues/wire.d.mts +9 -0
- package/queues/wire.mjs +199 -0
- package/s3.d.mts +43 -0
- package/s3.mjs +1003 -0
- package/schema-BF-Y1EVu.mjs +142 -0
- package/schema-DS8x1Dn8.mjs +115 -0
- package/schema-f7E9uzUn.d.mts +74 -0
- package/server-C7O7CICU.d.mts +182 -0
- package/socket.d.mts +67 -0
- package/socket.mjs +263 -0
- package/stand-in.d.mts +7 -0
- package/stand-in.mjs +185 -0
- package/store-BA8K-bAR.mjs +237 -0
- package/tabs-DEeMee7M.mjs +457 -0
- package/vector/contract.d.mts +10 -0
- package/vector/contract.mjs +635 -0
- package/vector/index.d.mts +2 -0
- package/vector/index.mjs +108 -0
- package/vector/stand-in.d.mts +11 -0
- package/vector/stand-in.mjs +169 -0
- package/vector/wire.d.mts +25 -0
- package/vector/wire.mjs +175 -0
- package/wire.d.mts +54 -0
- package/wire.mjs +731 -0
package/package.json
CHANGED
|
@@ -1,6 +1,154 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midsummer-ai/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-next.1723",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Midsummer's handles for an app's code: its Database, Storage, Queues, Vector DB, AI, Email and Agents, with stand-ins for tests.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "sol",
|
|
9
|
+
"url": "https://midsummer.new/orbit/midsummer/midsummer",
|
|
10
|
+
"directory": "platform/sdk"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://docs.midsummer.new",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./index.d.mts",
|
|
16
|
+
"import": "./index.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./stand-in": {
|
|
19
|
+
"types": "./stand-in.d.mts",
|
|
20
|
+
"import": "./stand-in.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./contract": {
|
|
23
|
+
"types": "./contract.d.mts",
|
|
24
|
+
"import": "./contract.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./wire": {
|
|
27
|
+
"types": "./wire.d.mts",
|
|
28
|
+
"import": "./wire.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./s3": {
|
|
31
|
+
"types": "./s3.d.mts",
|
|
32
|
+
"import": "./s3.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./socket": {
|
|
35
|
+
"types": "./socket.d.mts",
|
|
36
|
+
"import": "./socket.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./ai": {
|
|
39
|
+
"types": "./ai/index.d.mts",
|
|
40
|
+
"import": "./ai/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./ai/stand-in": {
|
|
43
|
+
"types": "./ai/stand-in.d.mts",
|
|
44
|
+
"import": "./ai/stand-in.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./ai/contract": {
|
|
47
|
+
"types": "./ai/contract.d.mts",
|
|
48
|
+
"import": "./ai/contract.mjs"
|
|
49
|
+
},
|
|
50
|
+
"./client": {
|
|
51
|
+
"types": "./client.d.mts",
|
|
52
|
+
"import": "./client.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./server": {
|
|
55
|
+
"types": "./database/server.d.mts",
|
|
56
|
+
"import": "./database/server.mjs"
|
|
57
|
+
},
|
|
58
|
+
"./database/build": {
|
|
59
|
+
"types": "./database/build.d.mts",
|
|
60
|
+
"import": "./database/build.mjs"
|
|
61
|
+
},
|
|
62
|
+
"./database/stand-in": {
|
|
63
|
+
"types": "./database/stand-in.d.mts",
|
|
64
|
+
"import": "./database/stand-in.mjs"
|
|
65
|
+
},
|
|
66
|
+
"./database/contract": {
|
|
67
|
+
"types": "./database/contract.d.mts",
|
|
68
|
+
"import": "./database/contract.mjs"
|
|
69
|
+
},
|
|
70
|
+
"./database/errors": {
|
|
71
|
+
"types": "./database/errors.d.mts",
|
|
72
|
+
"import": "./database/errors.mjs"
|
|
73
|
+
},
|
|
74
|
+
"./database": {
|
|
75
|
+
"types": "./database/client.d.mts",
|
|
76
|
+
"import": "./database/client.mjs"
|
|
77
|
+
},
|
|
78
|
+
"./agents": {
|
|
79
|
+
"types": "./agents/index.d.mts",
|
|
80
|
+
"import": "./agents/index.mjs"
|
|
81
|
+
},
|
|
82
|
+
"./agents/stand-in": {
|
|
83
|
+
"types": "./agents/stand-in.d.mts",
|
|
84
|
+
"import": "./agents/stand-in.mjs"
|
|
85
|
+
},
|
|
86
|
+
"./agents/contract": {
|
|
87
|
+
"types": "./agents/contract.d.mts",
|
|
88
|
+
"import": "./agents/contract.mjs"
|
|
89
|
+
},
|
|
90
|
+
"./queues": {
|
|
91
|
+
"types": "./queues/index.d.mts",
|
|
92
|
+
"import": "./queues/index.mjs"
|
|
93
|
+
},
|
|
94
|
+
"./queues/stand-in": {
|
|
95
|
+
"types": "./queues/stand-in.d.mts",
|
|
96
|
+
"import": "./queues/stand-in.mjs"
|
|
97
|
+
},
|
|
98
|
+
"./queues/contract": {
|
|
99
|
+
"types": "./queues/contract.d.mts",
|
|
100
|
+
"import": "./queues/contract.mjs"
|
|
101
|
+
},
|
|
102
|
+
"./queues/wire": {
|
|
103
|
+
"types": "./queues/wire.d.mts",
|
|
104
|
+
"import": "./queues/wire.mjs"
|
|
105
|
+
},
|
|
106
|
+
"./email": {
|
|
107
|
+
"types": "./email/index.d.mts",
|
|
108
|
+
"import": "./email/index.mjs"
|
|
109
|
+
},
|
|
110
|
+
"./email/stand-in": {
|
|
111
|
+
"types": "./email/stand-in.d.mts",
|
|
112
|
+
"import": "./email/stand-in.mjs"
|
|
113
|
+
},
|
|
114
|
+
"./email/contract": {
|
|
115
|
+
"types": "./email/contract.d.mts",
|
|
116
|
+
"import": "./email/contract.mjs"
|
|
117
|
+
},
|
|
118
|
+
"./email/client": {
|
|
119
|
+
"types": "./email/client.d.mts",
|
|
120
|
+
"import": "./email/client.mjs"
|
|
121
|
+
},
|
|
122
|
+
"./email/smtp": {
|
|
123
|
+
"types": "./email/smtp.d.mts",
|
|
124
|
+
"import": "./email/smtp.mjs"
|
|
125
|
+
},
|
|
126
|
+
"./vector": {
|
|
127
|
+
"types": "./vector/index.d.mts",
|
|
128
|
+
"import": "./vector/index.mjs"
|
|
129
|
+
},
|
|
130
|
+
"./vector/stand-in": {
|
|
131
|
+
"types": "./vector/stand-in.d.mts",
|
|
132
|
+
"import": "./vector/stand-in.mjs"
|
|
133
|
+
},
|
|
134
|
+
"./vector/contract": {
|
|
135
|
+
"types": "./vector/contract.d.mts",
|
|
136
|
+
"import": "./vector/contract.mjs"
|
|
137
|
+
},
|
|
138
|
+
"./vector/wire": {
|
|
139
|
+
"types": "./vector/wire.d.mts",
|
|
140
|
+
"import": "./vector/wire.mjs"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"files": [
|
|
144
|
+
"**/*.mjs",
|
|
145
|
+
"**/*.d.mts",
|
|
146
|
+
"README.md",
|
|
147
|
+
"LICENSE"
|
|
148
|
+
],
|
|
149
|
+
"dependencies": {
|
|
150
|
+
"acorn": "8.15.0",
|
|
151
|
+
"zod": "4.1.12"
|
|
152
|
+
},
|
|
153
|
+
"sideEffects": false
|
|
154
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
//#region src/database/client/predict.mjs
|
|
2
|
+
const SKIP = Symbol("a row not cached");
|
|
3
|
+
const plain = (v) => Object.fromEntries(Object.entries(v ?? {}).filter(([k]) => !k.startsWith("_")));
|
|
4
|
+
const copy = (v) => v == null ? v : structuredClone(v);
|
|
5
|
+
/** The token's `sub` (not verified: the units do that). */
|
|
6
|
+
const userOf = (token) => {
|
|
7
|
+
try {
|
|
8
|
+
const parts = String(token).split(".");
|
|
9
|
+
return JSON.parse(atob(parts[parts.length === 3 ? 1 : 0].replace(/-/gu, "+").replace(/_/gu, "/"))).sub ?? null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var Predictor = class {
|
|
15
|
+
/** `functions`: the app's functions module (its `mutations`), or the mutations object itself. */
|
|
16
|
+
constructor(g, functions) {
|
|
17
|
+
this.g = g;
|
|
18
|
+
this.fns = functions.mutations ?? functions;
|
|
19
|
+
this.pending = [];
|
|
20
|
+
this.layer = /* @__PURE__ */ new Map();
|
|
21
|
+
this.stats = {
|
|
22
|
+
predicted: 0,
|
|
23
|
+
skipped: 0,
|
|
24
|
+
refusedHere: 0,
|
|
25
|
+
rolledBack: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Called by `mutation` before it sends: `{ end(answer) }`, or null when nothing was predicted. */
|
|
29
|
+
begin(name, args, route = "") {
|
|
30
|
+
if (typeof this.fns[name] !== "function") return null;
|
|
31
|
+
const e = {
|
|
32
|
+
name,
|
|
33
|
+
args: copy(args),
|
|
34
|
+
route,
|
|
35
|
+
answered: false,
|
|
36
|
+
writes: null,
|
|
37
|
+
need: null
|
|
38
|
+
};
|
|
39
|
+
this.pending.push(e);
|
|
40
|
+
this.update();
|
|
41
|
+
if (!e.writes) {
|
|
42
|
+
this.pending.splice(this.pending.indexOf(e), 1);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
this.stats.predicted += 1;
|
|
46
|
+
return { end: (a) => this.end(e, a) };
|
|
47
|
+
}
|
|
48
|
+
end(e, a) {
|
|
49
|
+
if (!this.pending.includes(e)) return;
|
|
50
|
+
if (!a?.ok) {
|
|
51
|
+
this.stats.rolledBack += 1;
|
|
52
|
+
this.pending.splice(this.pending.indexOf(e), 1);
|
|
53
|
+
this.update();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
e.answered = true;
|
|
57
|
+
e.need = /* @__PURE__ */ new Map();
|
|
58
|
+
const wrote = new Map((a.wrote ?? []).map((r) => [`${r.t}/${r.id}`, r.v]));
|
|
59
|
+
for (const key of [...e.writes.keys()]) {
|
|
60
|
+
const held = this.g.rows.get(key);
|
|
61
|
+
if (!held) {
|
|
62
|
+
e.writes.delete(key);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
e.need.set(key, wrote.get(key) ?? held.v + 1);
|
|
66
|
+
}
|
|
67
|
+
this.caughtUp(e);
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
const k = this.pending.indexOf(e);
|
|
70
|
+
if (k >= 0) {
|
|
71
|
+
this.pending.splice(k, 1);
|
|
72
|
+
this.update();
|
|
73
|
+
}
|
|
74
|
+
}, 5e3).unref?.();
|
|
75
|
+
this.update();
|
|
76
|
+
}
|
|
77
|
+
/** A watch settled (connection.mjs `settled`): answered predictions whose rows caught up go; the rest run again. */
|
|
78
|
+
saw(w) {
|
|
79
|
+
if (w.q.k !== "g" || !this.pending.length) return;
|
|
80
|
+
for (const e of [...this.pending]) if (e.answered) this.caughtUp(e);
|
|
81
|
+
this.update();
|
|
82
|
+
}
|
|
83
|
+
caughtUp(e) {
|
|
84
|
+
for (const [key, v] of e.need) if ((this.g.rows.get(key)?.v ?? 0) >= v) {
|
|
85
|
+
e.need.delete(key);
|
|
86
|
+
e.writes.delete(key);
|
|
87
|
+
}
|
|
88
|
+
if (!e.writes.size) this.pending.splice(this.pending.indexOf(e), 1);
|
|
89
|
+
}
|
|
90
|
+
/** Runs every waiting prediction again, in order, and shows the result (only rows whose value changed are told). */
|
|
91
|
+
update() {
|
|
92
|
+
const layer = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const e of this.pending) {
|
|
94
|
+
if (!e.answered) e.writes = this.run(e, layer);
|
|
95
|
+
for (const [key, value] of e.writes ?? []) layer.set(key, value);
|
|
96
|
+
}
|
|
97
|
+
const g = this.g, changed = [];
|
|
98
|
+
for (const key of this.layer.keys()) if (!layer.has(key)) {
|
|
99
|
+
g.overlay.delete(key);
|
|
100
|
+
changed.push(key);
|
|
101
|
+
}
|
|
102
|
+
for (const [key, value] of layer) if (!this.layer.has(key) || JSON.stringify(this.layer.get(key)) !== JSON.stringify(value)) {
|
|
103
|
+
g.overlay.set(key, value);
|
|
104
|
+
changed.push(key);
|
|
105
|
+
}
|
|
106
|
+
this.layer = layer;
|
|
107
|
+
for (const key of changed) g.notifyRow(key);
|
|
108
|
+
}
|
|
109
|
+
/** One run of a mutation over the cache: its writes, or null (a row not cached, or the function threw). */
|
|
110
|
+
run(e, layer) {
|
|
111
|
+
const g = this.g, writes = /* @__PURE__ */ new Map();
|
|
112
|
+
const read = (tb, id) => {
|
|
113
|
+
const key = `${tb}/${id}`;
|
|
114
|
+
if (writes.has(key)) return copy(writes.get(key));
|
|
115
|
+
if (layer.has(key)) return copy(layer.get(key));
|
|
116
|
+
if (g.overlay.has(key) && !this.layer.has(key)) return copy(g.overlay.get(key));
|
|
117
|
+
let mine = null;
|
|
118
|
+
for (const m of [g.waiting, g.unconfirmed]) for (const { op } of m.values()) if (op.k === "s" && `${op.tb}/${op.id}` === key && (!mine || op.i > mine.i)) mine = op;
|
|
119
|
+
if (mine) return {
|
|
120
|
+
...copy(mine.value),
|
|
121
|
+
_id: id
|
|
122
|
+
};
|
|
123
|
+
const held = g.rows.get(key) ?? g.store?.row?.(key);
|
|
124
|
+
if (held) return copy(held.value);
|
|
125
|
+
throw SKIP;
|
|
126
|
+
};
|
|
127
|
+
const db = {
|
|
128
|
+
user: userOf(g.token),
|
|
129
|
+
server: false,
|
|
130
|
+
get: (tb, id) => read(tb, String(id)),
|
|
131
|
+
set: (tb, id, value) => {
|
|
132
|
+
writes.set(`${tb}/${id}`, {
|
|
133
|
+
...plain(value),
|
|
134
|
+
_id: String(id)
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
delete: (tb, id) => {
|
|
138
|
+
writes.set(`${tb}/${id}`, null);
|
|
139
|
+
},
|
|
140
|
+
credit: (tb, id, f, n) => {
|
|
141
|
+
let row;
|
|
142
|
+
try {
|
|
143
|
+
row = read(tb, String(id));
|
|
144
|
+
} catch (x) {
|
|
145
|
+
if (x === SKIP) return;
|
|
146
|
+
throw x;
|
|
147
|
+
}
|
|
148
|
+
db.set(tb, id, {
|
|
149
|
+
...row ?? {},
|
|
150
|
+
[f]: (Number(row?.[f]) || 0) + n
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
range: (tb, index, o = {}) => {
|
|
154
|
+
if (o.join) throw SKIP;
|
|
155
|
+
const key = `${g.unitOf(e.route)}|${JSON.stringify({
|
|
156
|
+
k: "r",
|
|
157
|
+
tb,
|
|
158
|
+
index,
|
|
159
|
+
...o
|
|
160
|
+
})}`;
|
|
161
|
+
const w = g.watches.get(key);
|
|
162
|
+
if (w?.state) return copy(w.state.ids.map((id) => w.state.rows.get(id)));
|
|
163
|
+
const old = g.store?.views?.get(key)?.view;
|
|
164
|
+
if (old) return copy(old);
|
|
165
|
+
throw SKIP;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
this.fns[e.name](db, copy(e.args) ?? {});
|
|
170
|
+
return writes;
|
|
171
|
+
} catch (x) {
|
|
172
|
+
if (x === SKIP) this.stats.skipped += 1;
|
|
173
|
+
else this.stats.refusedHere += 1;
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
//#endregion
|
|
179
|
+
export { Predictor };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Queues } from "./index.mjs";
|
|
2
|
+
//#region src/queues/contract.d.ts
|
|
3
|
+
interface QueueCase {
|
|
4
|
+
/** A line of the specification. */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
run(world: {
|
|
7
|
+
readonly queues: Queues;
|
|
8
|
+
advance(ms: number): void;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
declare const queuesContract: ReadonlyArray<QueueCase>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { QueueCase, queuesContract };
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { QueueError } from "./index.mjs";
|
|
2
|
+
//#region src/queues/contract.ts
|
|
3
|
+
/**
|
|
4
|
+
* Queues' contract: what every engine promises, as cases any test runner can run. The Stand-in runs
|
|
5
|
+
* them in this package; the platform runs the same cases against its own engine, through the wire.
|
|
6
|
+
* A behaviour that is not a case here is not promised.
|
|
7
|
+
*
|
|
8
|
+
* for (const c of queuesContract) test(c.name, () => { const time = clockAt(T0); return c.run({ queues: queues(makeEngine(time.now)), advance: time.advance }) })
|
|
9
|
+
*
|
|
10
|
+
* Each case gets Queues with nothing in them over an engine whose clock it moves with `advance`, and
|
|
11
|
+
* throws when the engine breaks the promise. Expected values are written out, never computed with the
|
|
12
|
+
* SDK's own code.
|
|
13
|
+
*/
|
|
14
|
+
const SECOND = 1e3;
|
|
15
|
+
const queuesContract = [
|
|
16
|
+
{
|
|
17
|
+
name: "a Message sent is received once with its body, and acked it is gone",
|
|
18
|
+
async run({ queues }) {
|
|
19
|
+
const emails = queues.queue("emails");
|
|
20
|
+
const { id } = await emails.send({
|
|
21
|
+
to: "ada@example.com",
|
|
22
|
+
n: [1, 2]
|
|
23
|
+
});
|
|
24
|
+
const [delivery, ...rest] = await emails.receive();
|
|
25
|
+
equal(rest.length, 0, "deliveries beyond the one sent");
|
|
26
|
+
equal(delivery?.id, id, "the delivery's id");
|
|
27
|
+
equal(delivery?.body, {
|
|
28
|
+
to: "ada@example.com",
|
|
29
|
+
n: [1, 2]
|
|
30
|
+
}, "the body");
|
|
31
|
+
equal(delivery?.attempt, 1, "the attempt");
|
|
32
|
+
equal((await emails.receive()).length, 0, "deliveries while the first is in flight");
|
|
33
|
+
await emails.ack(delivery.receipt);
|
|
34
|
+
equal(await emails.stats(), {
|
|
35
|
+
depth: 0,
|
|
36
|
+
ready: 0,
|
|
37
|
+
inFlight: 0,
|
|
38
|
+
delayed: 0,
|
|
39
|
+
dead: 0
|
|
40
|
+
}, "the stats once acked");
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "Messages sent in a batch are received in the order sent, up to the limit asked for",
|
|
45
|
+
async run({ queues }) {
|
|
46
|
+
const q = queues.queue("orders");
|
|
47
|
+
await q.sendBatch([
|
|
48
|
+
"a",
|
|
49
|
+
"b",
|
|
50
|
+
"c",
|
|
51
|
+
"d"
|
|
52
|
+
].map((body) => ({ body })));
|
|
53
|
+
equal((await q.receive({ limit: 3 })).map((d) => d.body), [
|
|
54
|
+
"a",
|
|
55
|
+
"b",
|
|
56
|
+
"c"
|
|
57
|
+
], "the first three");
|
|
58
|
+
equal((await q.receive({ limit: 3 })).map((d) => d.body), ["d"], "the rest");
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "a Message not acked within its visibility window is delivered again, and the old receipt does nothing",
|
|
63
|
+
async run({ queues, advance }) {
|
|
64
|
+
const q = queues.queue("jobs");
|
|
65
|
+
await q.send("work");
|
|
66
|
+
const [first] = await q.receive({ visibilitySeconds: 30 });
|
|
67
|
+
advance(29 * SECOND);
|
|
68
|
+
equal((await q.receive()).length, 0, "deliveries before the window ends");
|
|
69
|
+
advance(1 * SECOND);
|
|
70
|
+
const [second] = await q.receive();
|
|
71
|
+
equal(second?.attempt, 2, "the second delivery's attempt");
|
|
72
|
+
await q.ack(first.receipt);
|
|
73
|
+
await q.retry(first.receipt, { delaySeconds: 0 });
|
|
74
|
+
equal(await q.stats(), {
|
|
75
|
+
depth: 1,
|
|
76
|
+
ready: 0,
|
|
77
|
+
inFlight: 1,
|
|
78
|
+
delayed: 0,
|
|
79
|
+
oldestSentAt: Date.UTC(2026, 9, 27, 12),
|
|
80
|
+
dead: 0
|
|
81
|
+
}, "the stats after acking and retrying the ended delivery");
|
|
82
|
+
await q.ack(second.receipt);
|
|
83
|
+
equal((await q.stats()).depth, 0, "the depth after acking the current one");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "a receipt whose visibility window has ended does nothing, before the Message is delivered again too",
|
|
88
|
+
async run({ queues, advance }) {
|
|
89
|
+
const q = queues.queue("jobs");
|
|
90
|
+
await q.send("work");
|
|
91
|
+
const [first] = await q.receive({ visibilitySeconds: 30 });
|
|
92
|
+
advance(30 * SECOND);
|
|
93
|
+
await q.ack(first.receipt);
|
|
94
|
+
await q.retry(first.receipt, { delaySeconds: 600 });
|
|
95
|
+
const [second] = await q.receive();
|
|
96
|
+
equal(second?.attempt, 2, "the delivery after a late ack and a late retry");
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "a retried Message waits 10 seconds after its first delivery, 20 after its second, and as long as asked when a delay is given",
|
|
101
|
+
async run({ queues, advance }) {
|
|
102
|
+
const q = queues.queue("jobs");
|
|
103
|
+
await q.send("work");
|
|
104
|
+
await q.retry((await q.receive())[0].receipt);
|
|
105
|
+
advance(9 * SECOND);
|
|
106
|
+
equal((await q.receive()).length, 0, "deliveries 9 seconds after the first retry");
|
|
107
|
+
advance(1 * SECOND);
|
|
108
|
+
const [second] = await q.receive();
|
|
109
|
+
equal(second?.attempt, 2, "the attempt 10 seconds after");
|
|
110
|
+
await q.retry(second.receipt);
|
|
111
|
+
advance(19 * SECOND);
|
|
112
|
+
equal((await q.receive()).length, 0, "deliveries 19 seconds after the second retry");
|
|
113
|
+
advance(1 * SECOND);
|
|
114
|
+
const [third] = await q.receive();
|
|
115
|
+
equal(third?.attempt, 3, "the attempt 20 seconds after");
|
|
116
|
+
await q.retry(third.receipt, { delaySeconds: 0 });
|
|
117
|
+
equal((await q.receive())[0]?.attempt, 4, "the attempt after a retry with no delay");
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "the backoff stops growing at 10 minutes",
|
|
122
|
+
async run({ queues, advance }) {
|
|
123
|
+
const q = queues.queue("jobs");
|
|
124
|
+
await q.send("work");
|
|
125
|
+
for (let attempt = 1; attempt <= 8; attempt += 1) {
|
|
126
|
+
const [delivery] = await q.receive({ maxAttempts: 100 });
|
|
127
|
+
equal(delivery?.attempt, attempt, `delivery ${attempt}`);
|
|
128
|
+
await q.retry(delivery.receipt);
|
|
129
|
+
advance(600 * SECOND);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "a Message delivered maxAttempts times moves to the dead-letter Queue when retried or left, keeping its body and id",
|
|
135
|
+
async run({ queues, advance }) {
|
|
136
|
+
const q = queues.queue("payments");
|
|
137
|
+
const { id: retried } = await q.send({ charge: 1 });
|
|
138
|
+
const { id: left } = await q.send({ charge: 2 }, { delaySeconds: 1 });
|
|
139
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
140
|
+
const [delivery] = await q.receive({
|
|
141
|
+
maxAttempts: 2,
|
|
142
|
+
limit: 1
|
|
143
|
+
});
|
|
144
|
+
await q.retry(delivery.receipt, { delaySeconds: 0 });
|
|
145
|
+
}
|
|
146
|
+
advance(1 * SECOND);
|
|
147
|
+
await q.receive({
|
|
148
|
+
maxAttempts: 1,
|
|
149
|
+
visibilitySeconds: 5
|
|
150
|
+
});
|
|
151
|
+
advance(5 * SECOND);
|
|
152
|
+
equal((await q.receive()).length, 0, "deliveries of either from the Queue itself");
|
|
153
|
+
equal(await q.stats(), {
|
|
154
|
+
depth: 0,
|
|
155
|
+
ready: 0,
|
|
156
|
+
inFlight: 0,
|
|
157
|
+
delayed: 0,
|
|
158
|
+
dead: 2
|
|
159
|
+
}, "the Queue's stats");
|
|
160
|
+
equal((await q.deadLetter.receive()).map((d) => [d.id, d.body]), [[retried, { charge: 1 }], [left, { charge: 2 }]], "the dead-letter Queue's Messages");
|
|
161
|
+
equal(q.deadLetter.name, "payments.dead", "the dead-letter Queue's name");
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "a dead-letter Queue's Messages are retried there and never move on",
|
|
166
|
+
async run({ queues }) {
|
|
167
|
+
const q = queues.queue("payments");
|
|
168
|
+
await q.send("x");
|
|
169
|
+
await q.retry((await q.receive({ maxAttempts: 1 }))[0].receipt);
|
|
170
|
+
for (let i = 0; i < 3; i += 1) await q.deadLetter.retry((await q.deadLetter.receive({ maxAttempts: 1 }))[0].receipt, { delaySeconds: 0 });
|
|
171
|
+
equal((await q.deadLetter.stats()).depth, 1, "the dead-letter Queue's depth");
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "a Message sent with a delay is received only once the delay is over, and counts as delayed until then",
|
|
176
|
+
async run({ queues, advance }) {
|
|
177
|
+
const q = queues.queue("reminders");
|
|
178
|
+
await q.send("later", { delaySeconds: 60 });
|
|
179
|
+
equal(await q.stats(), {
|
|
180
|
+
depth: 1,
|
|
181
|
+
ready: 0,
|
|
182
|
+
inFlight: 0,
|
|
183
|
+
delayed: 1,
|
|
184
|
+
oldestSentAt: Date.UTC(2026, 9, 27, 12),
|
|
185
|
+
dead: 0
|
|
186
|
+
}, "the stats while delayed");
|
|
187
|
+
advance(59 * SECOND);
|
|
188
|
+
equal((await q.receive()).length, 0, "deliveries a second early");
|
|
189
|
+
advance(1 * SECOND);
|
|
190
|
+
equal((await q.receive()).map((d) => d.body), ["later"], "deliveries once due");
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "stats count ready, in-flight and delayed Messages and say when the oldest was sent",
|
|
195
|
+
async run({ queues, advance }) {
|
|
196
|
+
const q = queues.queue("events");
|
|
197
|
+
await q.send(1);
|
|
198
|
+
advance(5 * SECOND);
|
|
199
|
+
await q.sendBatch([{ body: 2 }, {
|
|
200
|
+
body: 3,
|
|
201
|
+
delaySeconds: 30
|
|
202
|
+
}]);
|
|
203
|
+
await q.receive({ limit: 1 });
|
|
204
|
+
equal(await q.stats(), {
|
|
205
|
+
depth: 3,
|
|
206
|
+
ready: 1,
|
|
207
|
+
inFlight: 1,
|
|
208
|
+
delayed: 1,
|
|
209
|
+
oldestSentAt: Date.UTC(2026, 9, 27, 12),
|
|
210
|
+
dead: 0
|
|
211
|
+
}, "the stats");
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "a send with a key already sent to that Queue answers the first id and sends nothing; another Queue keeps its own keys",
|
|
216
|
+
async run({ queues }) {
|
|
217
|
+
const q = queues.queue("welcome");
|
|
218
|
+
const first = await q.send("hi", { key: "user:ada" });
|
|
219
|
+
equal((await q.send("hi again", { key: "user:ada" })).id, first.id, "the id of the second send");
|
|
220
|
+
equal((await q.stats()).depth, 1, "the depth");
|
|
221
|
+
await queues.queue("other").send("hi", { key: "user:ada" });
|
|
222
|
+
equal((await queues.queue("other").stats()).depth, 1, "the other Queue's depth");
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "Queues are apart: what one holds, another never delivers",
|
|
227
|
+
async run({ queues }) {
|
|
228
|
+
await queues.queue("a").send("for a");
|
|
229
|
+
equal((await queues.queue("b").receive()).length, 0, "deliveries from b");
|
|
230
|
+
equal((await queues.queue("a").receive()).map((d) => d.body), ["for a"], "deliveries from a");
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "a bad name, body, delay, limit or receipt is invalid",
|
|
235
|
+
async run({ queues }) {
|
|
236
|
+
fails(() => queues.queue("Has Spaces"), "a name with spaces");
|
|
237
|
+
fails(() => queues.queue("a.dead"), "a name with a dot");
|
|
238
|
+
const q = queues.queue("ok");
|
|
239
|
+
await rejects(q.send(void 0), "an undefined body");
|
|
240
|
+
await rejects(q.send("x".repeat(131071)), "a body over 128 KiB as JSON");
|
|
241
|
+
await rejects(q.send("x", { delaySeconds: 43201 }), "a delay over 12 hours");
|
|
242
|
+
await rejects(q.send("x", { key: "" }), "an empty key");
|
|
243
|
+
await rejects(q.receive({ limit: 101 }), "a limit over 100");
|
|
244
|
+
await rejects(q.receive({ visibilitySeconds: 0 }), "a visibility of 0");
|
|
245
|
+
await rejects(q.ack("not a receipt"), "a receipt with spaces");
|
|
246
|
+
await rejects(q.sendBatch([]), "an empty batch");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
function equal(actual, expected, what) {
|
|
251
|
+
const a = JSON.stringify(sorted(actual)), e = JSON.stringify(sorted(expected));
|
|
252
|
+
if (a !== e) throw new Error(`${what}: expected ${e}, got ${a}`);
|
|
253
|
+
}
|
|
254
|
+
function sorted(value) {
|
|
255
|
+
if (Array.isArray(value)) return value.map(sorted);
|
|
256
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : 1).map(([k, v]) => [k, sorted(v)]));
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
function fails(run, what) {
|
|
260
|
+
try {
|
|
261
|
+
run();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error instanceof QueueError && error.code === "invalid") return;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
throw new Error(`${what}: expected invalid`);
|
|
267
|
+
}
|
|
268
|
+
async function rejects(promise, what) {
|
|
269
|
+
try {
|
|
270
|
+
await promise;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error instanceof QueueError && error.code === "invalid") return;
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
throw new Error(`${what}: expected invalid`);
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
export { queuesContract };
|