@cosmicdrift/kumiko-dispatcher-live 0.113.1 → 1.0.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dispatcher-live",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "HTTP-only Dispatcher for Kumiko UIs. Always-online; failures surface immediately. Default client for web admin/cockpit apps.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
27
|
+
"@cosmicdrift/kumiko-headless": "1.0.0"
|
|
28
28
|
},
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"registry": "https://registry.npmjs.org",
|
|
@@ -46,10 +46,7 @@ describe("createLiveDispatcher", () => {
|
|
|
46
46
|
expect(headers["Content-Type"]).toBe("application/json");
|
|
47
47
|
expect(headers["X-CSRF-Token"]).toBe("csrf-abc");
|
|
48
48
|
const body = JSON.parse(call?.init.body as string);
|
|
49
|
-
expect(body).
|
|
50
|
-
// Auto-generated idempotency key (#761) — always present.
|
|
51
|
-
expect(typeof body.requestId).toBe("string");
|
|
52
|
-
expect(body.requestId.length).toBeGreaterThan(8);
|
|
49
|
+
expect(body).toEqual({ type: "app:write:task:create", payload: { title: "hello" } });
|
|
53
50
|
});
|
|
54
51
|
|
|
55
52
|
test("write: propagates requestId into body when provided", async () => {
|
|
@@ -62,43 +59,6 @@ describe("createLiveDispatcher", () => {
|
|
|
62
59
|
expect(body.requestId).toBe("idem-99");
|
|
63
60
|
});
|
|
64
61
|
|
|
65
|
-
test("write: generates a fresh requestId per invocation when none provided (#761)", async () => {
|
|
66
|
-
const { fetch, calls } = makeFetch({ body: { isSuccess: true, data: {} } });
|
|
67
|
-
const disp = createLiveDispatcher({ fetch, readCsrf: () => "t" });
|
|
68
|
-
|
|
69
|
-
await disp.write("app:write:x:create", { a: 1 });
|
|
70
|
-
await disp.write("app:write:x:create", { a: 1 });
|
|
71
|
-
|
|
72
|
-
const first = JSON.parse(calls[0]?.init.body as string).requestId;
|
|
73
|
-
const second = JSON.parse(calls[1]?.init.body as string).requestId;
|
|
74
|
-
expect(typeof first).toBe("string");
|
|
75
|
-
expect(typeof second).toBe("string");
|
|
76
|
-
expect(first).not.toBe(second);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("batch: generates one requestId for the whole batch when none provided (#761)", async () => {
|
|
80
|
-
const { fetch, calls } = makeFetch({ body: { isSuccess: true, results: [] } });
|
|
81
|
-
const disp = createLiveDispatcher({ fetch, readCsrf: () => "t" });
|
|
82
|
-
|
|
83
|
-
await disp.batch([
|
|
84
|
-
{ type: "a", payload: {} },
|
|
85
|
-
{ type: "b", payload: {} },
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
const body = JSON.parse(calls[0]?.init.body as string);
|
|
89
|
-
expect(typeof body.requestId).toBe("string");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("batch: propagates an explicit requestId (#761)", async () => {
|
|
93
|
-
const { fetch, calls } = makeFetch({ body: { isSuccess: true, results: [] } });
|
|
94
|
-
const disp = createLiveDispatcher({ fetch, readCsrf: () => "t" });
|
|
95
|
-
|
|
96
|
-
await disp.batch([{ type: "a", payload: {} }], { requestId: "batch-7" });
|
|
97
|
-
|
|
98
|
-
const body = JSON.parse(calls[0]?.init.body as string);
|
|
99
|
-
expect(body.requestId).toBe("batch-7");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
62
|
test("write: no CSRF token → header omitted, request still fires (server will 401/csrf-mismatch)", async () => {
|
|
103
63
|
const { fetch, calls } = makeFetch({ body: { isSuccess: true, data: {} } });
|
|
104
64
|
const disp = createLiveDispatcher({ fetch, readCsrf: () => undefined });
|
package/src/dispatcher-live.ts
CHANGED
|
@@ -154,11 +154,7 @@ export function createLiveDispatcher(options: LiveDispatcherOptions = {}): Dispa
|
|
|
154
154
|
opts?: WriteOpts,
|
|
155
155
|
): Promise<WriteResult<TData>> {
|
|
156
156
|
const body: Record<string, unknown> = { type, payload };
|
|
157
|
-
|
|
158
|
-
// dedup never engages and a transport-level double-send duplicates
|
|
159
|
-
// events. Callers with a logical-submit id (savable queue, form
|
|
160
|
-
// controllers) pass their own and keep it across their retries.
|
|
161
|
-
body["requestId"] = opts?.requestId ?? generateRequestId();
|
|
157
|
+
if (opts?.requestId) body["requestId"] = opts.requestId;
|
|
162
158
|
const call = await callJson(PATH_WRITE, body, opts?.signal);
|
|
163
159
|
return normalizeWriteResult<TData>(call);
|
|
164
160
|
},
|
|
@@ -175,10 +171,7 @@ export function createLiveDispatcher(options: LiveDispatcherOptions = {}): Dispa
|
|
|
175
171
|
|
|
176
172
|
async batch(commands: readonly Command[], opts?: WriteOpts): Promise<BatchResult> {
|
|
177
173
|
const body: Record<string, unknown> = { commands };
|
|
178
|
-
|
|
179
|
-
// under it, so a retried batch returns the cached outcome instead of
|
|
180
|
-
// re-executing the commands (#761).
|
|
181
|
-
body["requestId"] = opts?.requestId ?? generateRequestId();
|
|
174
|
+
if (opts?.requestId) body["requestId"] = opts.requestId;
|
|
182
175
|
const call = await callJson(PATH_BATCH, body, opts?.signal);
|
|
183
176
|
return normalizeBatchResponse(call);
|
|
184
177
|
},
|
|
@@ -196,16 +189,6 @@ export function createLiveDispatcher(options: LiveDispatcherOptions = {}): Dispa
|
|
|
196
189
|
const EMPTY_PENDING_WRITES: readonly PendingWrite[] = Object.freeze([]);
|
|
197
190
|
const EMPTY_PENDING_FILES: readonly PendingFile[] = Object.freeze([]);
|
|
198
191
|
|
|
199
|
-
// crypto.randomUUID where available (browser, Bun, Node); Math.random
|
|
200
|
-
// fallback for React-Native runtimes without the WebCrypto polyfill.
|
|
201
|
-
// Uniqueness only needs to hold per user within the server's dedup window —
|
|
202
|
-
// this is an idempotency key, not a security token.
|
|
203
|
-
function generateRequestId(): string {
|
|
204
|
-
const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
|
|
205
|
-
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
206
|
-
return `req-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
192
|
type CallOutcome =
|
|
210
193
|
| { readonly ok: true; readonly body: unknown; readonly status: number }
|
|
211
194
|
| { readonly ok: false; readonly networkFailure: DispatcherError };
|