@sixthsummit/ship-testing 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +32 -2
- package/lib/cache.d.ts +43 -0
- package/lib/cache.d.ts.map +1 -0
- package/lib/cache.js +199 -0
- package/lib/cache.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +9 -0
- package/lib/index.js.map +1 -0
- package/lib/jobs-conformance.d.ts +35 -0
- package/lib/jobs-conformance.d.ts.map +1 -0
- package/lib/jobs-conformance.js +232 -0
- package/lib/jobs-conformance.js.map +1 -0
- package/lib/jobs.d.ts +64 -0
- package/lib/jobs.d.ts.map +1 -0
- package/lib/jobs.js +499 -0
- package/lib/jobs.js.map +1 -0
- package/lib/outbox.d.ts +35 -0
- package/lib/outbox.d.ts.map +1 -0
- package/lib/outbox.js +218 -0
- package/lib/outbox.js.map +1 -0
- package/lib/parity.d.ts +52 -0
- package/lib/parity.d.ts.map +1 -0
- package/lib/parity.js +72 -0
- package/lib/parity.js.map +1 -0
- package/lib/postgres.d.ts +93 -0
- package/lib/postgres.d.ts.map +1 -0
- package/lib/postgres.js +83 -0
- package/lib/postgres.js.map +1 -0
- package/lib/realtime.d.ts +93 -0
- package/lib/realtime.d.ts.map +1 -0
- package/lib/realtime.js +0 -0
- package/lib/realtime.js.map +1 -0
- package/lib/webhooks.d.ts +48 -0
- package/lib/webhooks.d.ts.map +1 -0
- package/lib/webhooks.js +109 -0
- package/lib/webhooks.js.map +1 -0
- package/package.json +94 -3
package/lib/outbox.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { isOutbox, systemActor } from "@sixthsummit/ship-core";
|
|
2
|
+
export function createInMemoryOutbox(outbox) {
|
|
3
|
+
if (!isOutbox(outbox)) {
|
|
4
|
+
throw new TypeError("createInMemoryOutbox requires an outbox created by defineOutbox()");
|
|
5
|
+
}
|
|
6
|
+
let rows = [];
|
|
7
|
+
let sequence = 0;
|
|
8
|
+
const outboxId = outbox.id;
|
|
9
|
+
const find = (id) => rows.findIndex((row) => row.id === id);
|
|
10
|
+
const replace = (index, patch) => {
|
|
11
|
+
const next = Object.freeze({ ...rows[index], ...patch });
|
|
12
|
+
rows[index] = next;
|
|
13
|
+
return next;
|
|
14
|
+
};
|
|
15
|
+
const record = async (_scope, intents) => Object.freeze(intents.map((intent) => {
|
|
16
|
+
if (intent.dedupeKey !== undefined &&
|
|
17
|
+
rows.some((row) => row.intentId === intent.intentId && row.dedupeKey === intent.dedupeKey)) {
|
|
18
|
+
return Object.freeze({ deduplicated: true });
|
|
19
|
+
}
|
|
20
|
+
sequence += 1;
|
|
21
|
+
const id = `${outboxId}:${sequence}`;
|
|
22
|
+
rows.push(Object.freeze({
|
|
23
|
+
id,
|
|
24
|
+
outboxId,
|
|
25
|
+
intentId: intent.intentId,
|
|
26
|
+
payload: intent.payload,
|
|
27
|
+
...(intent.dedupeKey !== undefined ? { dedupeKey: intent.dedupeKey } : {}),
|
|
28
|
+
status: "pending",
|
|
29
|
+
attempts: 0,
|
|
30
|
+
availableAt: intent.availableAt,
|
|
31
|
+
createdAt: new Date(),
|
|
32
|
+
}));
|
|
33
|
+
return Object.freeze({ recordId: id, deduplicated: false });
|
|
34
|
+
}));
|
|
35
|
+
const claimMatching = async (now, batchSize, leaseMs, matchesIntent) => {
|
|
36
|
+
const claimed = [];
|
|
37
|
+
const due = rows
|
|
38
|
+
.map((row, index) => ({ row, index }))
|
|
39
|
+
.filter(({ row }) => matchesIntent(row.intentId) &&
|
|
40
|
+
((row.status === "pending" && row.availableAt.getTime() <= now.getTime()) ||
|
|
41
|
+
(row.status === "claimed" && (row.leaseExpiresAt?.getTime() ?? 0) <= now.getTime())))
|
|
42
|
+
.sort((left, right) => left.row.availableAt.getTime() - right.row.availableAt.getTime())
|
|
43
|
+
.slice(0, batchSize);
|
|
44
|
+
for (const { index } of due) {
|
|
45
|
+
claimed.push(replace(index, {
|
|
46
|
+
status: "claimed",
|
|
47
|
+
leaseExpiresAt: new Date(now.getTime() + leaseMs),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
return Object.freeze(claimed);
|
|
51
|
+
};
|
|
52
|
+
const claimDue = (now, batchSize, leaseMs, intentIds) => claimMatching(now, batchSize, leaseMs, (intentId) => intentIds === undefined ? true : intentIds.includes(intentId));
|
|
53
|
+
const claimUnknownDue = (now, batchSize, leaseMs, knownIntentIds) => claimMatching(now, batchSize, leaseMs, (intentId) => !knownIntentIds.includes(intentId));
|
|
54
|
+
const complete = async (recordId, now) => {
|
|
55
|
+
const index = find(recordId);
|
|
56
|
+
if (index >= 0 && rows[index].status === "claimed") {
|
|
57
|
+
replace(index, { status: "completed", completedAt: now, leaseExpiresAt: undefined });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const fail = async (recordId, error, disposition, now) => {
|
|
61
|
+
const index = find(recordId);
|
|
62
|
+
if (index < 0 || rows[index].status !== "claimed")
|
|
63
|
+
return;
|
|
64
|
+
const attempts = rows[index].attempts + 1;
|
|
65
|
+
if ("deadLetter" in disposition) {
|
|
66
|
+
replace(index, {
|
|
67
|
+
status: "dead-lettered",
|
|
68
|
+
attempts,
|
|
69
|
+
lastError: error,
|
|
70
|
+
deadLetteredAt: now,
|
|
71
|
+
leaseExpiresAt: undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
replace(index, {
|
|
76
|
+
status: "pending",
|
|
77
|
+
attempts,
|
|
78
|
+
lastError: error,
|
|
79
|
+
availableAt: disposition.retryAt,
|
|
80
|
+
leaseExpiresAt: undefined,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const listDeadLettered = async ({ limit }) => Object.freeze(rows.filter((row) => row.status === "dead-lettered").slice(0, limit));
|
|
85
|
+
const retry = async (recordId, now) => {
|
|
86
|
+
const index = find(recordId);
|
|
87
|
+
if (index < 0 || rows[index].status !== "dead-lettered")
|
|
88
|
+
return undefined;
|
|
89
|
+
return replace(index, { status: "pending", availableAt: now, deadLetteredAt: undefined });
|
|
90
|
+
};
|
|
91
|
+
const discard = async (recordId, reason, now) => {
|
|
92
|
+
const index = find(recordId);
|
|
93
|
+
if (index < 0 || rows[index].status !== "dead-lettered")
|
|
94
|
+
return undefined;
|
|
95
|
+
return replace(index, { status: "discarded", discardedAt: now, discardReason: reason });
|
|
96
|
+
};
|
|
97
|
+
const cleanup = async (retention, now) => {
|
|
98
|
+
let completedDeleted = 0;
|
|
99
|
+
let deadLetteredDeleted = 0;
|
|
100
|
+
rows = rows.filter((row) => {
|
|
101
|
+
if (row.status === "completed" &&
|
|
102
|
+
row.completedAt !== undefined &&
|
|
103
|
+
now.getTime() - row.completedAt.getTime() >= retention.completedMs) {
|
|
104
|
+
completedDeleted += 1;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const terminalAt = row.status === "dead-lettered" ? row.deadLetteredAt : row.discardedAt;
|
|
108
|
+
if ((row.status === "dead-lettered" || row.status === "discarded") &&
|
|
109
|
+
terminalAt !== undefined &&
|
|
110
|
+
now.getTime() - terminalAt.getTime() >= retention.deadLetteredMs) {
|
|
111
|
+
deadLetteredDeleted += 1;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
return Object.freeze({ completedDeleted, deadLetteredDeleted });
|
|
117
|
+
};
|
|
118
|
+
const health = async (now) => {
|
|
119
|
+
const pending = rows.filter((row) => row.status === "pending");
|
|
120
|
+
const oldest = pending.reduce((best, row) => (best === undefined || row.availableAt < best ? row.availableAt : best), undefined);
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
pending: pending.length,
|
|
123
|
+
claimed: rows.filter((row) => row.status === "claimed" && (row.leaseExpiresAt?.getTime() ?? 0) > now.getTime()).length,
|
|
124
|
+
deadLettered: rows.filter((row) => row.status === "dead-lettered").length,
|
|
125
|
+
...(oldest ? { oldestPendingAt: oldest } : {}),
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
kind: "ship.outbox-backend",
|
|
130
|
+
adapter: "memory",
|
|
131
|
+
outboxId,
|
|
132
|
+
record,
|
|
133
|
+
claimDue,
|
|
134
|
+
claimUnknownDue,
|
|
135
|
+
complete,
|
|
136
|
+
fail,
|
|
137
|
+
listDeadLettered,
|
|
138
|
+
retry,
|
|
139
|
+
discard,
|
|
140
|
+
cleanup,
|
|
141
|
+
health,
|
|
142
|
+
records: () => Object.freeze([...rows]),
|
|
143
|
+
reset: () => {
|
|
144
|
+
rows = [];
|
|
145
|
+
sequence = 0;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export function createInMemoryIdempotencyStore() {
|
|
150
|
+
const entries = new Map();
|
|
151
|
+
const claim = async (key, ttlMs, context) => {
|
|
152
|
+
const existing = entries.get(key);
|
|
153
|
+
if (existing && existing.expiresAt > context.now.getTime()) {
|
|
154
|
+
return existing.status === "completed" ? "duplicate" : "in-flight";
|
|
155
|
+
}
|
|
156
|
+
entries.set(key, { status: "in-flight", expiresAt: context.now.getTime() + ttlMs });
|
|
157
|
+
return "claimed";
|
|
158
|
+
};
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
kind: "ship.idempotency-store",
|
|
161
|
+
adapter: "memory",
|
|
162
|
+
claim,
|
|
163
|
+
complete: async (key, result) => {
|
|
164
|
+
const existing = entries.get(key);
|
|
165
|
+
if (existing)
|
|
166
|
+
entries.set(key, { ...existing, status: "completed", result });
|
|
167
|
+
},
|
|
168
|
+
release: async (key) => {
|
|
169
|
+
if (entries.get(key)?.status === "in-flight")
|
|
170
|
+
entries.delete(key);
|
|
171
|
+
},
|
|
172
|
+
readResult: async (key, context) => {
|
|
173
|
+
const existing = entries.get(key);
|
|
174
|
+
return existing?.status === "completed" && existing.expiresAt > context.now.getTime()
|
|
175
|
+
? existing.result
|
|
176
|
+
: undefined;
|
|
177
|
+
},
|
|
178
|
+
reset: () => entries.clear(),
|
|
179
|
+
purgeExpired: (before, prefix = "") => {
|
|
180
|
+
let purged = 0;
|
|
181
|
+
for (const [key, entry] of entries) {
|
|
182
|
+
if (key.startsWith(prefix) && entry.expiresAt < before.getTime()) {
|
|
183
|
+
entries.delete(key);
|
|
184
|
+
purged += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return purged;
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Runs `<outbox>.dispatch` until a pass claims nothing and returns the totals.
|
|
193
|
+
* Pair it with `createInMemoryOutbox()` in unit tests to assert on effects.
|
|
194
|
+
*/
|
|
195
|
+
export async function drainOutbox(executor, outbox, options = {}) {
|
|
196
|
+
const outboxId = typeof outbox === "string" ? outbox : outbox.id;
|
|
197
|
+
const operationId = `${outboxId}.dispatch`;
|
|
198
|
+
const actor = options.actor ?? systemActor({ id: "ship.testing.drain" });
|
|
199
|
+
const maxRuns = options.maxRuns ?? 100;
|
|
200
|
+
const totals = { batchSize: 0, claimed: 0, completed: 0, retried: 0, deadLettered: 0 };
|
|
201
|
+
for (let run = 0; run < maxRuns; run += 1) {
|
|
202
|
+
const result = await executor.run(operationId, {}, { actor });
|
|
203
|
+
if (!result.ok) {
|
|
204
|
+
throw new Error(`drainOutbox: ${operationId} failed with ${result.error.code}: ${result.error.message}`);
|
|
205
|
+
}
|
|
206
|
+
const pass = result.data;
|
|
207
|
+
// The dispatcher's batch size, reported once; the totals sum the passes.
|
|
208
|
+
totals.batchSize = pass.batchSize;
|
|
209
|
+
totals.claimed += pass.claimed;
|
|
210
|
+
totals.completed += pass.completed;
|
|
211
|
+
totals.retried += pass.retried;
|
|
212
|
+
totals.deadLettered += pass.deadLettered;
|
|
213
|
+
if (pass.claimed === 0)
|
|
214
|
+
return Object.freeze(totals);
|
|
215
|
+
}
|
|
216
|
+
throw new Error(`drainOutbox: ${operationId} still claimed records after ${maxRuns} runs`);
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=outbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.js","sourceRoot":"","sources":["../src/outbox.ts"],"names":[],"mappings":"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"}
|
package/lib/parity.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3↔v4 behavioral-parity harness (A2-6).
|
|
3
|
+
*
|
|
4
|
+
* Exercises a v4 app with a recorded request and asserts the response matches a
|
|
5
|
+
* set of checks — either a v4-conformance contract or a behavior captured from a
|
|
6
|
+
* live v3 app (the parity oracle). Pure + in-process (Hono `app.request`), no
|
|
7
|
+
* Docker. Shipped via `@sixthsummit/ship-testing` so a consumer's migrated
|
|
8
|
+
* app can run its own captured fixtures. See docs/v4/tooling/parity-harness.md.
|
|
9
|
+
*/
|
|
10
|
+
/** Anything with Hono's `.request()` — the v4 app under test. */
|
|
11
|
+
export interface RequestableApp {
|
|
12
|
+
request(path: string, init?: RequestInit): Promise<Response>;
|
|
13
|
+
}
|
|
14
|
+
export interface ParityCheck {
|
|
15
|
+
/** Dot-path into the captured response: `status`, `headers.<lowercased>`, `body.<...>`. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Deep-equality assertion. */
|
|
18
|
+
equals?: unknown;
|
|
19
|
+
/** Presence assertion (`true` = must be present, `false` = must be absent). */
|
|
20
|
+
exists?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ParityFixture {
|
|
23
|
+
id: string;
|
|
24
|
+
metadata: {
|
|
25
|
+
area: string;
|
|
26
|
+
/** The behavioral contract under test, e.g. "401 on missing token". */
|
|
27
|
+
surface: string;
|
|
28
|
+
/**
|
|
29
|
+
* `v4-conformance` = encodes a documented v4 contract (ships green).
|
|
30
|
+
* `v3-capture` = recorded from a live v3 app; asserts the v4 build matches.
|
|
31
|
+
*/
|
|
32
|
+
source: "v3-capture" | "v4-conformance";
|
|
33
|
+
[k: string]: unknown;
|
|
34
|
+
};
|
|
35
|
+
request: {
|
|
36
|
+
method: string;
|
|
37
|
+
path: string;
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
body?: unknown;
|
|
40
|
+
};
|
|
41
|
+
checks: ParityCheck[];
|
|
42
|
+
}
|
|
43
|
+
export interface ParityResult {
|
|
44
|
+
fixture: ParityFixture;
|
|
45
|
+
passed: boolean;
|
|
46
|
+
failures: string[];
|
|
47
|
+
}
|
|
48
|
+
/** Parse a JSONL fixture file (skips blank lines + `#` comments). */
|
|
49
|
+
export declare function parseParityFixtures(jsonl: string): ParityFixture[];
|
|
50
|
+
/** Exercise `app` with the fixture's request and evaluate its checks. */
|
|
51
|
+
export declare function runParityFixture(app: RequestableApp, fixture: ParityFixture): Promise<ParityResult>;
|
|
52
|
+
//# sourceMappingURL=parity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parity.d.ts","sourceRoot":"","sources":["../src/parity.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,WAAW;IAC1B,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,uEAAuE;QACvE,OAAO,EAAE,MAAM,CAAC;QAChB;;;WAGG;QACH,MAAM,EAAE,YAAY,GAAG,gBAAgB,CAAC;QACxC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;KACtB,CAAC;IACF,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;IACF,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAMlE;AAiBD,yEAAyE;AACzE,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,YAAY,CAAC,CAgDvB"}
|
package/lib/parity.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3↔v4 behavioral-parity harness (A2-6).
|
|
3
|
+
*
|
|
4
|
+
* Exercises a v4 app with a recorded request and asserts the response matches a
|
|
5
|
+
* set of checks — either a v4-conformance contract or a behavior captured from a
|
|
6
|
+
* live v3 app (the parity oracle). Pure + in-process (Hono `app.request`), no
|
|
7
|
+
* Docker. Shipped via `@sixthsummit/ship-testing` so a consumer's migrated
|
|
8
|
+
* app can run its own captured fixtures. See docs/v4/tooling/parity-harness.md.
|
|
9
|
+
*/
|
|
10
|
+
/** Parse a JSONL fixture file (skips blank lines + `#` comments). */
|
|
11
|
+
export function parseParityFixtures(jsonl) {
|
|
12
|
+
return jsonl
|
|
13
|
+
.split("\n")
|
|
14
|
+
.map((line) => line.trim())
|
|
15
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"))
|
|
16
|
+
.map((line) => JSON.parse(line));
|
|
17
|
+
}
|
|
18
|
+
function resolvePath(captured, path) {
|
|
19
|
+
let cur = captured;
|
|
20
|
+
for (const key of path.split(".")) {
|
|
21
|
+
if (cur == null || typeof cur !== "object")
|
|
22
|
+
return undefined;
|
|
23
|
+
cur = cur[key];
|
|
24
|
+
}
|
|
25
|
+
return cur;
|
|
26
|
+
}
|
|
27
|
+
/** Exercise `app` with the fixture's request and evaluate its checks. */
|
|
28
|
+
export async function runParityFixture(app, fixture) {
|
|
29
|
+
const { method, path, headers, body } = fixture.request;
|
|
30
|
+
const init = { method };
|
|
31
|
+
if (body !== undefined && body !== null) {
|
|
32
|
+
init.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
33
|
+
init.headers = { "content-type": "application/json", ...(headers ?? {}) };
|
|
34
|
+
}
|
|
35
|
+
else if (headers) {
|
|
36
|
+
init.headers = headers;
|
|
37
|
+
}
|
|
38
|
+
const res = await app.request(path, init);
|
|
39
|
+
const rawText = await res.text();
|
|
40
|
+
let parsedBody = rawText;
|
|
41
|
+
if (rawText.length > 0) {
|
|
42
|
+
try {
|
|
43
|
+
parsedBody = JSON.parse(rawText);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* non-JSON body — keep the raw text */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
parsedBody = null;
|
|
51
|
+
}
|
|
52
|
+
const captured = {
|
|
53
|
+
status: res.status,
|
|
54
|
+
headers: Object.fromEntries([...res.headers.entries()].map(([k, v]) => [k.toLowerCase(), v])),
|
|
55
|
+
body: parsedBody,
|
|
56
|
+
};
|
|
57
|
+
const failures = [];
|
|
58
|
+
for (const check of fixture.checks) {
|
|
59
|
+
const actual = resolvePath(captured, check.path);
|
|
60
|
+
if (check.exists !== undefined) {
|
|
61
|
+
const present = actual !== undefined;
|
|
62
|
+
if (present !== check.exists) {
|
|
63
|
+
failures.push(`${check.path}: expected ${check.exists ? "present" : "absent"}, got ${present ? "present" : "absent"}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (check.equals !== undefined && JSON.stringify(actual) !== JSON.stringify(check.equals)) {
|
|
67
|
+
failures.push(`${check.path}: expected ${JSON.stringify(check.equals)}, got ${JSON.stringify(actual)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { fixture, passed: failures.length === 0, failures };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=parity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parity.js","sourceRoot":"","sources":["../src/parity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
|
|
2
|
+
import { type NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import pg from "pg";
|
|
4
|
+
/**
|
|
5
|
+
* Real-Postgres test harness — spins up an ephemeral Postgres
|
|
6
|
+
* container, builds a Drizzle client against it, and pushes the
|
|
7
|
+
* supplied Drizzle schema so the tables exist before tests run.
|
|
8
|
+
*
|
|
9
|
+
* Why testcontainers: the per-module unit tests (accounts /
|
|
10
|
+
* oauth / outbox) drive `DrizzleEntityService` against
|
|
11
|
+
* a fake Drizzle-shaped DB so the test loop is fast and Docker-
|
|
12
|
+
* free. Integration tests under `*.integration.test.ts` exercise
|
|
13
|
+
* the SAME services against a real Postgres so the SQL the
|
|
14
|
+
* framework actually emits — partial indexes, FK cascades,
|
|
15
|
+
* `onConflictDoUpdate`, transactional outbox semantics — gets
|
|
16
|
+
* verified end-to-end. Different surface; both matter.
|
|
17
|
+
*
|
|
18
|
+
* This harness is the foundation for V4_STATUS G3. First
|
|
19
|
+
* integration test ships in this commit; oauth + outbox
|
|
20
|
+
* integration tests layer on in subsequent slices.
|
|
21
|
+
*
|
|
22
|
+
* Usage (suite-scoped, one container per describe block):
|
|
23
|
+
*
|
|
24
|
+
* import { describe, beforeAll, afterAll, it, expect } from "vitest";
|
|
25
|
+
* import { setupPostgresTestDb, type PostgresTestDb }
|
|
26
|
+
* from "@sixthsummit/ship-testing/postgres";
|
|
27
|
+
* import { accountsSchema } from "../../../../../../modules/accounts/index.js";
|
|
28
|
+
*
|
|
29
|
+
* describe("accounts integration", () => {
|
|
30
|
+
* let dbCtx: PostgresTestDb<typeof accountsSchema>;
|
|
31
|
+
* beforeAll(async () => {
|
|
32
|
+
* dbCtx = await setupPostgresTestDb(accountsSchema);
|
|
33
|
+
* }, 60_000);
|
|
34
|
+
* afterAll(async () => { await dbCtx.cleanup(); });
|
|
35
|
+
*
|
|
36
|
+
* it("inserts and reads", async () => {
|
|
37
|
+
* // dbCtx.db is a fully typed NodePgDatabase<typeof schema>.
|
|
38
|
+
* });
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* The container starts in ~3–10 s on a warm Docker daemon. Tests
|
|
42
|
+
* SHOULD share a container per describe block (suite-scoped
|
|
43
|
+
* lifecycle); per-test container overhead would dominate runtime.
|
|
44
|
+
* Per-test isolation comes from `truncateAll(dbCtx)` between
|
|
45
|
+
* tests, not from a fresh container.
|
|
46
|
+
*
|
|
47
|
+
* NOT included by the default `pnpm test` run — the root Vitest
|
|
48
|
+
* config excludes `*.integration.test.ts`. Run via
|
|
49
|
+
* `pnpm run test:integration`.
|
|
50
|
+
*/
|
|
51
|
+
export interface PostgresTestDb<TSchema extends Record<string, unknown>> {
|
|
52
|
+
/** Started Postgres container (testcontainers handle). */
|
|
53
|
+
container: StartedPostgreSqlContainer;
|
|
54
|
+
/** Underlying pg.Pool. Surface for raw queries when Drizzle's API doesn't fit. */
|
|
55
|
+
pool: pg.Pool;
|
|
56
|
+
/** Fully-typed Drizzle client over the supplied schema. */
|
|
57
|
+
db: NodePgDatabase<TSchema>;
|
|
58
|
+
/** The connection string; useful when spawning subprocesses. */
|
|
59
|
+
connectionString: string;
|
|
60
|
+
/** Tear down the container + close the pool. Idempotent. */
|
|
61
|
+
cleanup: () => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Bring up a Postgres container, build a Drizzle client, and push
|
|
65
|
+
* the supplied schema so the tables exist. Returns the test-db
|
|
66
|
+
* context plus a cleanup hook.
|
|
67
|
+
*
|
|
68
|
+
* The schema arg is a `Record<string, unknown>` matching the shape
|
|
69
|
+
* of `import * as schema from "..."` — drizzle-kit's `pushSchema`
|
|
70
|
+
* walks the imports and creates every `pgTable` it finds, plus
|
|
71
|
+
* any `pgEnum`s, indexes, and foreign keys.
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* A `pg.Pool` for a test against the disposable container. Attaches the
|
|
75
|
+
* pool-level `error` listener that `pg` requires: when the container stops
|
|
76
|
+
* while an idle client is still connected, the server sends
|
|
77
|
+
* `57P01 terminating connection due to administrator command`, and without
|
|
78
|
+
* a listener that arrives as an uncaught exception after the suite has
|
|
79
|
+
* already passed. Tests that build their own pools should use this too.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createPostgresTestPool(connectionString: string, options?: Omit<pg.PoolConfig, "connectionString">): pg.Pool;
|
|
82
|
+
export declare function setupPostgresTestDb<TSchema extends Record<string, unknown>>(schema: TSchema): Promise<PostgresTestDb<TSchema>>;
|
|
83
|
+
/**
|
|
84
|
+
* Truncate every table in the supplied schema, restarting identity
|
|
85
|
+
* columns. Use between tests in a suite to isolate per-test state
|
|
86
|
+
* without paying the container-startup cost.
|
|
87
|
+
*
|
|
88
|
+
* Iterates every value in the schema export that has a `_.name`
|
|
89
|
+
* property (the convention drizzle-orm uses for `pgTable` metadata).
|
|
90
|
+
* Skips relations, enums, and any other non-table exports.
|
|
91
|
+
*/
|
|
92
|
+
export declare function truncateAll<TSchema extends Record<string, unknown>>(ctx: PostgresTestDb<TSchema>): Promise<void>;
|
|
93
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAElG,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,MAAM,WAAW,cAAc,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACrE,0DAA0D;IAC1D,SAAS,EAAE,0BAA0B,CAAC;IACtC,kFAAkF;IAClF,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC;IACd,2DAA2D;IAC3D,EAAE,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC5B,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,gBAAgB,EAAE,MAAM,EACxB,OAAO,GAAE,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,kBAAkB,CAAM,GACpD,EAAE,CAAC,IAAI,CAIT;AAED,wBAAsB,mBAAmB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAkClC;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvE,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC,CAWf"}
|
package/lib/postgres.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { PostgreSqlContainer } from "@testcontainers/postgresql";
|
|
2
|
+
import { pushSchema } from "drizzle-kit/api";
|
|
3
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
4
|
+
import pg from "pg";
|
|
5
|
+
/**
|
|
6
|
+
* Bring up a Postgres container, build a Drizzle client, and push
|
|
7
|
+
* the supplied schema so the tables exist. Returns the test-db
|
|
8
|
+
* context plus a cleanup hook.
|
|
9
|
+
*
|
|
10
|
+
* The schema arg is a `Record<string, unknown>` matching the shape
|
|
11
|
+
* of `import * as schema from "..."` — drizzle-kit's `pushSchema`
|
|
12
|
+
* walks the imports and creates every `pgTable` it finds, plus
|
|
13
|
+
* any `pgEnum`s, indexes, and foreign keys.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A `pg.Pool` for a test against the disposable container. Attaches the
|
|
17
|
+
* pool-level `error` listener that `pg` requires: when the container stops
|
|
18
|
+
* while an idle client is still connected, the server sends
|
|
19
|
+
* `57P01 terminating connection due to administrator command`, and without
|
|
20
|
+
* a listener that arrives as an uncaught exception after the suite has
|
|
21
|
+
* already passed. Tests that build their own pools should use this too.
|
|
22
|
+
*/
|
|
23
|
+
export function createPostgresTestPool(connectionString, options = {}) {
|
|
24
|
+
const pool = new pg.Pool({ connectionString, ...options });
|
|
25
|
+
pool.on("error", () => undefined);
|
|
26
|
+
return pool;
|
|
27
|
+
}
|
|
28
|
+
export async function setupPostgresTestDb(schema) {
|
|
29
|
+
const container = await new PostgreSqlContainer("postgres:16-alpine")
|
|
30
|
+
.withDatabase("ship_api_test")
|
|
31
|
+
.withUsername("ship_api")
|
|
32
|
+
.withPassword("ship_api")
|
|
33
|
+
.start();
|
|
34
|
+
const connectionString = container.getConnectionUri();
|
|
35
|
+
const pool = createPostgresTestPool(connectionString, { max: 5 });
|
|
36
|
+
const db = drizzle(pool, { schema });
|
|
37
|
+
// Programmatic pushSchema — same machinery `drizzle-kit push`
|
|
38
|
+
// uses, no migration files needed. The framework's schema source
|
|
39
|
+
// (.ts files) is the single source of truth for both runtime and
|
|
40
|
+
// tests. Schema changes propagate without committing intermediate
|
|
41
|
+
// migration SQL the test harness would otherwise have to track.
|
|
42
|
+
//
|
|
43
|
+
// The cast is required because drizzle-kit's `pushSchema` is
|
|
44
|
+
// typed against a dialect-agnostic `PgDatabase<any, ...>` rather
|
|
45
|
+
// than the concrete `NodePgDatabase<TSchema>` the runtime uses.
|
|
46
|
+
// Both are functionally compatible.
|
|
47
|
+
const result = await pushSchema(schema, db);
|
|
48
|
+
await result.apply();
|
|
49
|
+
return {
|
|
50
|
+
container,
|
|
51
|
+
pool,
|
|
52
|
+
db,
|
|
53
|
+
connectionString,
|
|
54
|
+
cleanup: async () => {
|
|
55
|
+
await pool.end().catch(() => undefined);
|
|
56
|
+
await container.stop().catch(() => undefined);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Truncate every table in the supplied schema, restarting identity
|
|
62
|
+
* columns. Use between tests in a suite to isolate per-test state
|
|
63
|
+
* without paying the container-startup cost.
|
|
64
|
+
*
|
|
65
|
+
* Iterates every value in the schema export that has a `_.name`
|
|
66
|
+
* property (the convention drizzle-orm uses for `pgTable` metadata).
|
|
67
|
+
* Skips relations, enums, and any other non-table exports.
|
|
68
|
+
*/
|
|
69
|
+
export async function truncateAll(ctx) {
|
|
70
|
+
const tableNames = [];
|
|
71
|
+
for (const exported of Object.values(ctx.db._.schema ?? {})) {
|
|
72
|
+
const meta = exported?.dbName;
|
|
73
|
+
if (typeof meta === "string" && meta.length > 0)
|
|
74
|
+
tableNames.push(meta);
|
|
75
|
+
}
|
|
76
|
+
if (tableNames.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
// Single TRUNCATE statement covers all tables; CASCADE handles FK
|
|
79
|
+
// references; RESTART IDENTITY resets serial / identity sequences.
|
|
80
|
+
const quoted = tableNames.map((n) => `"${n}"`).join(", ");
|
|
81
|
+
await ctx.pool.query(`TRUNCATE TABLE ${quoted} RESTART IDENTITY CASCADE`);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAmC,MAAM,4BAA4B,CAAC;AAClG,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,MAAM,IAAI,CAAC;AA+DpB;;;;;;;;;GASG;AACH;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,gBAAwB,EACxB,UAAmD,EAAE;IAErD,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAe;IAEf,MAAM,SAAS,GAAG,MAAM,IAAI,mBAAmB,CAAC,oBAAoB,CAAC;SAClE,YAAY,CAAC,eAAe,CAAC;SAC7B,YAAY,CAAC,UAAU,CAAC;SACxB,YAAY,CAAC,UAAU,CAAC;SACxB,KAAK,EAAE,CAAC;IAEX,MAAM,gBAAgB,GAAG,SAAS,CAAC,gBAAgB,EAAE,CAAC;IACtD,MAAM,IAAI,GAAG,sBAAsB,CAAC,gBAAgB,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAClE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAErC,8DAA8D;IAC9D,iEAAiE;IACjE,iEAAiE;IACjE,kEAAkE;IAClE,gEAAgE;IAChE,EAAE;IACF,6DAA6D;IAC7D,iEAAiE;IACjE,gEAAgE;IAChE,oCAAoC;IACpC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,EAAiD,CAAC,CAAC;IAC3F,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IAErB,OAAO;QACL,SAAS;QACT,IAAI;QACJ,EAAE;QACF,gBAAgB;QAChB,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACxC,MAAM,SAAS,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAChD,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAA4B;IAE5B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,GAAI,QAA4C,EAAE,MAAM,CAAC;QACnE,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACpC,kEAAkE;IAClE,mEAAmE;IACnE,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,MAAM,2BAA2B,CAAC,CAAC;AAC5E,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AnyChannel, AnyModule, ApplicationExecutor, CompiledApplication, OutboxDispatchResult, RealtimeBackend, RuntimeSchema, TenantScopeAuthority } from "@sixthsummit/ship-core";
|
|
2
|
+
import { type DrainOutboxOptions } from "./outbox.js";
|
|
3
|
+
export interface RealtimePublication {
|
|
4
|
+
readonly channelId: string;
|
|
5
|
+
readonly scopeKey: string;
|
|
6
|
+
readonly payload: unknown;
|
|
7
|
+
readonly publishedAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface InMemoryRealtimeBackendOptions {
|
|
10
|
+
readonly channels: readonly AnyChannel[];
|
|
11
|
+
readonly tenantScopes?: TenantScopeAuthority;
|
|
12
|
+
readonly id?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Deterministic process-local realtime backend for unit tests and the raw
|
|
16
|
+
* WebSocket smoke fixture. Delivery is synchronous, presence is held in
|
|
17
|
+
* memory, and every accepted publication is recorded for assertions.
|
|
18
|
+
*/
|
|
19
|
+
export interface InMemoryRealtimeBackend extends RealtimeBackend {
|
|
20
|
+
readonly adapter: "memory";
|
|
21
|
+
readonly published: readonly RealtimePublication[];
|
|
22
|
+
/** Registers additional channels after construction. */
|
|
23
|
+
register(channels: readonly AnyChannel[]): void;
|
|
24
|
+
reset(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function createInMemoryRealtimeBackend(options: InMemoryRealtimeBackendOptions): InMemoryRealtimeBackend;
|
|
27
|
+
export interface CollectedPublishes<TApp extends CompiledApplication<string, readonly AnyModule[]>> {
|
|
28
|
+
/** The same application with every channel publication bound to the recording backend. */
|
|
29
|
+
readonly application: TApp;
|
|
30
|
+
readonly backend: InMemoryRealtimeBackend;
|
|
31
|
+
/** Publications accepted so far, oldest first. */
|
|
32
|
+
publishes(): readonly RealtimePublication[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Recompiles an application so every channel publication provider records
|
|
36
|
+
* into one in-memory backend, without asking the app factory to accept a
|
|
37
|
+
* backend parameter. Other providers are kept as declared.
|
|
38
|
+
*/
|
|
39
|
+
export declare function collectPublishes<TApp extends CompiledApplication<string, readonly AnyModule[]>>(application: TApp, options?: {
|
|
40
|
+
readonly tenantScopes?: TenantScopeAuthority;
|
|
41
|
+
}): CollectedPublishes<TApp>;
|
|
42
|
+
/**
|
|
43
|
+
* Runs `ship-realtime.publication.dispatch` until nothing is claimed, so
|
|
44
|
+
* publications recorded by `transaction: "write"` operations reach the
|
|
45
|
+
* backend `collectPublishes()` records into. The application must mount
|
|
46
|
+
* `defineRealtimePublicationModule()`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function drainRealtimePublications(executor: ApplicationExecutor<CompiledApplication<string, readonly AnyModule[]>>, options?: DrainOutboxOptions): Promise<OutboxDispatchResult>;
|
|
49
|
+
export type ConformanceTest = (name: string, run: () => Promise<void>) => unknown;
|
|
50
|
+
export interface RealtimeConformanceOptions {
|
|
51
|
+
/** Builds a fresh backend registered with the suite's fixture channels. */
|
|
52
|
+
readonly createBackend: (channels: readonly AnyChannel[], options: {
|
|
53
|
+
readonly tenantScopes: TenantScopeAuthority;
|
|
54
|
+
}) => RealtimeBackend | Promise<RealtimeBackend>;
|
|
55
|
+
/** The host runner's test registration function, e.g. vitest `it`. */
|
|
56
|
+
readonly test: ConformanceTest;
|
|
57
|
+
/** Upper bound for asynchronous delivery. Defaults to 2000 ms. */
|
|
58
|
+
readonly deliveryTimeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const conformanceChannels: Readonly<{
|
|
61
|
+
feed: import("@sixthsummit/ship-core").Channel<"conformance.feed", "actor", RuntimeSchema<{
|
|
62
|
+
readonly n: number;
|
|
63
|
+
}, {
|
|
64
|
+
readonly n: number;
|
|
65
|
+
}>, false>;
|
|
66
|
+
room: import("@sixthsummit/ship-core").Channel<"conformance.room", "tenant", RuntimeSchema<{
|
|
67
|
+
readonly n: number;
|
|
68
|
+
}, {
|
|
69
|
+
readonly n: number;
|
|
70
|
+
}>, true>;
|
|
71
|
+
board: import("@sixthsummit/ship-core").Channel<"conformance.board", "resource", RuntimeSchema<{
|
|
72
|
+
readonly n: number;
|
|
73
|
+
}, {
|
|
74
|
+
readonly n: number;
|
|
75
|
+
}>, true>;
|
|
76
|
+
ticker: import("@sixthsummit/ship-core").Channel<"conformance.ticker", "actor", RuntimeSchema<{
|
|
77
|
+
readonly id: string;
|
|
78
|
+
readonly n: number;
|
|
79
|
+
}, {
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly n: number;
|
|
82
|
+
}>, false>;
|
|
83
|
+
}>;
|
|
84
|
+
export declare const conformanceTenantScopes: TenantScopeAuthority;
|
|
85
|
+
/**
|
|
86
|
+
* Registers the behavioral contract every RealtimeBackend must satisfy:
|
|
87
|
+
* authorized delivery, fail-closed subscription authorization, per-scope
|
|
88
|
+
* isolation and sequencing, presence dedupe, coalescing, disconnect
|
|
89
|
+
* cleanup, and health. Both the in-memory backend and the Socket.IO
|
|
90
|
+
* adapter run exactly this suite.
|
|
91
|
+
*/
|
|
92
|
+
export declare function realtimeBackendConformance(options: RealtimeConformanceOptions): void;
|
|
93
|
+
//# sourceMappingURL=realtime.d.ts.map
|