@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.113.1",
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.113.1"
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).toMatchObject({ type: "app:write:task:create", payload: { title: "hello" } });
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 });
@@ -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
- // Idempotency by default (#761): without a requestId the server-side
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
- // One id for the whole batch — the server caches the BatchResult
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 };