@rindle/optimistic 0.2.0 → 0.3.1
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/dist/backend.d.ts +43 -1
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +157 -83
- package/dist/backend.js.map +1 -1
- package/dist/client-id.d.ts +3 -0
- package/dist/client-id.d.ts.map +1 -1
- package/dist/client-id.js +17 -0
- package/dist/client-id.js.map +1 -1
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +46 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/backend.ts +176 -81
- package/src/client-id.ts +16 -0
- package/src/client.ts +58 -5
- package/src/index.ts +3 -1
package/src/client.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { initWasm } from "@rindle/wasm";
|
|
|
20
20
|
|
|
21
21
|
import type { OptimisticBackend } from "./backend.ts";
|
|
22
22
|
import type { ClientRegistry, MutationTx } from "./backend.ts";
|
|
23
|
-
import { stableClientID } from "./client-id.ts";
|
|
23
|
+
import { resetStableClientID, stableClientID } from "./client-id.ts";
|
|
24
24
|
import { createOptimisticStore, type MutateFn } from "./index.ts";
|
|
25
25
|
import type { Store } from "@rindle/client";
|
|
26
26
|
|
|
@@ -58,6 +58,13 @@ export interface RindleClientOptions<S extends ColsMap, R extends ClientRegistry
|
|
|
58
58
|
/** A policy rejection's reason (the prediction's snap-back rides the lmid release). */
|
|
59
59
|
onRejected?: (envelope: MutationEnvelope, reason: string) => void;
|
|
60
60
|
queue?: { maxBatch?: number; retryDelayMs?: (attempt: number) => number };
|
|
61
|
+
/** Development-only recovery knobs. Keep off in production: a mutation gap means state loss
|
|
62
|
+
* or two writers sharing a clientID, and should be investigated. */
|
|
63
|
+
dev?: {
|
|
64
|
+
/** On a mutation-gap response, clear the persisted clientID and hard reload the page. This
|
|
65
|
+
* recovers from dev DB wipes while making the reset visible to the developer. */
|
|
66
|
+
resetOnMutationGap?: boolean;
|
|
67
|
+
};
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export interface RindleClient<S extends ColsMap, R extends ClientRegistry> {
|
|
@@ -90,8 +97,9 @@ export async function createRindleClient<S extends ColsMap, R extends ClientRegi
|
|
|
90
97
|
headers: { "content-type": "application/json", ...extra },
|
|
91
98
|
body: JSON.stringify(payload),
|
|
92
99
|
});
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
if (!res.ok) throw new RindleApiHttpError(path, res.status, text);
|
|
102
|
+
return text ? JSON.parse(text) : undefined;
|
|
95
103
|
};
|
|
96
104
|
|
|
97
105
|
// Reads-leg connection: a fixed transport (tests/in-process) or a replaceable connection built
|
|
@@ -116,8 +124,15 @@ export async function createRindleClient<S extends ColsMap, R extends ClientRegi
|
|
|
116
124
|
retryDelayMs: opts.queue?.retryDelayMs,
|
|
117
125
|
onRejected: opts.onRejected,
|
|
118
126
|
send: async (envelopes) => {
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
try {
|
|
128
|
+
const outcomes = (await post(routes.mutate, { envelopes })) as Array<{ accepted: boolean; reason?: string }>;
|
|
129
|
+
return outcomes.map((o): PushOutcome => ({ accepted: o.accepted, reason: o.reason }));
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (opts.dev?.resetOnMutationGap && reloadAfterMutationGap(err)) {
|
|
132
|
+
return new Promise<never>(() => {});
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
121
136
|
},
|
|
122
137
|
}),
|
|
123
138
|
});
|
|
@@ -154,3 +169,41 @@ export async function createRindleClient<S extends ColsMap, R extends ClientRegi
|
|
|
154
169
|
|
|
155
170
|
export type { ClientRegistry, MutationTx };
|
|
156
171
|
export type { MutateFn } from "./index.ts";
|
|
172
|
+
|
|
173
|
+
class RindleApiHttpError extends Error {
|
|
174
|
+
readonly path: string;
|
|
175
|
+
readonly status: number;
|
|
176
|
+
readonly body: string;
|
|
177
|
+
|
|
178
|
+
constructor(path: string, status: number, body: string) {
|
|
179
|
+
super(`rindle api ${path} failed: ${status}${body ? ` ${body}` : ""}`);
|
|
180
|
+
this.name = "RindleApiHttpError";
|
|
181
|
+
this.path = path;
|
|
182
|
+
this.status = status;
|
|
183
|
+
this.body = body;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const MUTATION_GAP_RE = /\bmutation(?: id)? gap\b/i;
|
|
188
|
+
|
|
189
|
+
function reloadAfterMutationGap(err: unknown): boolean {
|
|
190
|
+
const text =
|
|
191
|
+
err instanceof RindleApiHttpError
|
|
192
|
+
? `${err.status} ${err.body} ${err.message}`
|
|
193
|
+
: String((err as Error)?.message ?? err);
|
|
194
|
+
if (!MUTATION_GAP_RE.test(text)) return false;
|
|
195
|
+
|
|
196
|
+
resetStableClientID();
|
|
197
|
+
console.error(
|
|
198
|
+
"[rindle] mutation gap detected; cleared the dev client id and reloading so this tab starts a fresh mutation stream.",
|
|
199
|
+
err,
|
|
200
|
+
);
|
|
201
|
+
const loc = (globalThis as unknown as { location?: { reload?: () => void } }).location;
|
|
202
|
+
if (typeof loc?.reload !== "function") return false;
|
|
203
|
+
try {
|
|
204
|
+
loc.reload();
|
|
205
|
+
return true;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type {
|
|
|
34
34
|
OptimisticBackendOptions,
|
|
35
35
|
OptimisticInspect,
|
|
36
36
|
PendingInspect,
|
|
37
|
+
QueryArg,
|
|
38
|
+
QueryResultRow,
|
|
37
39
|
ResultType,
|
|
38
40
|
} from "./backend.ts";
|
|
39
41
|
export type { MutationEnvelope, OptimisticSource, ProgressFrame } from "@rindle/client";
|
|
@@ -41,7 +43,7 @@ export type { MutationEnvelope, OptimisticSource, ProgressFrame } from "@rindle/
|
|
|
41
43
|
// The one-call client for the daemon/serverless topology (API server + daemon ws).
|
|
42
44
|
export { createRindleClient } from "./client.ts";
|
|
43
45
|
export type { RindleClient, RindleClientOptions } from "./client.ts";
|
|
44
|
-
export { stableClientID } from "./client-id.ts";
|
|
46
|
+
export { resetStableClientID, stableClientID } from "./client-id.ts";
|
|
45
47
|
|
|
46
48
|
/** A {@link Store} over an {@link OptimisticBackend}, plus the named-mutator entry
|
|
47
49
|
* (`mutate.createIssue(args)` — the §9 dream shape) and the §6 lifecycle surface. */
|