slopkit 0.1.0 → 0.1.2
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/DEPS.md +1 -1
- package/dist/conformance/push-batch.test.d.ts +2 -0
- package/dist/conformance/push-batch.test.js +66 -0
- package/dist/sync.d.ts +4 -0
- package/dist/sync.js +8 -0
- package/package.json +1 -1
package/DEPS.md
CHANGED
|
@@ -29,7 +29,7 @@ seeded network's PRNG and scheduler.
|
|
|
29
29
|
## Budgets
|
|
30
30
|
|
|
31
31
|
- **Kit client core**, gated by `pack:check` on the packed package: `schema`, `contract`, `sync`, `client`, `machine`,
|
|
32
|
-
`money`, `digest`, `states` and `web/replica`, minified and gzipped together, ≤ 10,730 bytes (10,
|
|
32
|
+
`money`, `digest`, `states` and `web/replica`, minified and gzipped together, ≤ 10,730 bytes (10,726 measured).
|
|
33
33
|
`cron` is browser-safe but outside the core; an app that ships it counts it in its own budget. The budget grows only
|
|
34
34
|
with a new entry or guarantee, by its measured size.
|
|
35
35
|
- **Hello app** (preact, signals, and `sync`, `client`, `web/replica`, `schema`, `contract`, `states`): ≤ 17 KB gzip
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** A push carries at most `PUSH_MAX` commands, so a long outbox drains through a push endpoint bounded the same way. */
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import { createClient } from "../client.js";
|
|
4
|
+
import { createSync, PUSH_MAX } from "../sync.js";
|
|
5
|
+
import { copy, database, patch, pending, sync } from "./fixtures.js";
|
|
6
|
+
test("120 kept commands go out as pushes of 50, 50 and 20, each once, and all reach an outcome", async () => {
|
|
7
|
+
const stored = database();
|
|
8
|
+
await stored.port.load();
|
|
9
|
+
await stored.port.commit({
|
|
10
|
+
replica: copy(29, []),
|
|
11
|
+
reset: true,
|
|
12
|
+
base: 0,
|
|
13
|
+
replaces: "dataset-a",
|
|
14
|
+
ops: [],
|
|
15
|
+
});
|
|
16
|
+
for (let id = 1; id <= 120; id++) {
|
|
17
|
+
await stored.port.keep({ ...pending("older-page", id, id), since: 29 });
|
|
18
|
+
}
|
|
19
|
+
const reopened = database({ factory: stored.factory, name: stored.name });
|
|
20
|
+
const pushes = [];
|
|
21
|
+
const runtime = createClient({
|
|
22
|
+
sync,
|
|
23
|
+
client: "newer-page",
|
|
24
|
+
ports: {
|
|
25
|
+
storage: reopened.port,
|
|
26
|
+
network: {
|
|
27
|
+
pull: async () => ({ ok: true, value: { patch: patch(29, []), receipts: [] } }),
|
|
28
|
+
push: async (request) => {
|
|
29
|
+
pushes.push(request.commands.map((each) => each.id));
|
|
30
|
+
// The server's contract: a push of more than 50 commands is refused before anything acts.
|
|
31
|
+
if (request.commands.length > 50) {
|
|
32
|
+
return { ok: false, failure: { kind: "invalid", message: "commands: at most 50" } };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
value: {
|
|
37
|
+
receipts: request.commands.map((each) => ({
|
|
38
|
+
id: each.id,
|
|
39
|
+
outcome: { kind: "ok", resource: `r${each.id}` },
|
|
40
|
+
revision: 29,
|
|
41
|
+
})),
|
|
42
|
+
revision: 29,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
now: () => 0,
|
|
48
|
+
fingerprint: async () => "fixture-fingerprint",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
runtime.start();
|
|
52
|
+
await runtime.settle();
|
|
53
|
+
expect(pushes.map((each) => each.length)).toStrictEqual([50, 50, 20]);
|
|
54
|
+
expect(pushes.flat()).toStrictEqual(Array.from({ length: 120 }, (_, at) => at + 1));
|
|
55
|
+
expect(runtime
|
|
56
|
+
.model()
|
|
57
|
+
.commands.filter((each) => each.state.kind === "done")
|
|
58
|
+
.map((each) => each.id)).toHaveLength(120);
|
|
59
|
+
expect(runtime.faults).toStrictEqual([]);
|
|
60
|
+
expect(PUSH_MAX).toBe(50);
|
|
61
|
+
});
|
|
62
|
+
test("a push bound that isn't a positive integer is refused when sync is made", () => {
|
|
63
|
+
for (const pushMax of [0, -1, 1.5, Number.NaN]) {
|
|
64
|
+
expect(() => createSync({ tables: [], empty: {}, onlineOnly: () => false, refused: () => null, pushMax })).toThrow(/pushMax must be a positive integer/);
|
|
65
|
+
}
|
|
66
|
+
});
|
package/dist/sync.d.ts
CHANGED
|
@@ -378,6 +378,8 @@ export type Spec<T, P, S, C, O> = {
|
|
|
378
378
|
* until it resolves or the person dismisses it. Default: every outcome is resolved.
|
|
379
379
|
*/
|
|
380
380
|
resolved?: (pending: Pending<C, O>, model: Model<T, S, C, O>) => boolean;
|
|
381
|
+
/** The most commands one push carries: the push endpoint's own bound. Default `PUSH_MAX`. */
|
|
382
|
+
pushMax?: number;
|
|
381
383
|
};
|
|
382
384
|
/** The socket must be heard this often (the server pokes every 20 s); quieter, the page polls. */
|
|
383
385
|
export declare const SOCKET_DEADLINE_MS = 45000;
|
|
@@ -385,6 +387,8 @@ export declare const SOCKET_DEADLINE_MS = 45000;
|
|
|
385
387
|
export declare const POLL_MS = 4000;
|
|
386
388
|
/** Storage gets this long to load, or to keep an intent, before the page goes on without it. */
|
|
387
389
|
export declare const STORAGE_DEADLINE_MS = 3000;
|
|
390
|
+
/** Commands per push unless the spec says otherwise; the rest wait for the next push. */
|
|
391
|
+
export declare const PUSH_MAX = 50;
|
|
388
392
|
/** Settled commands stay drawn this long. */
|
|
389
393
|
export declare const DONE_KEEP_MS = 60000;
|
|
390
394
|
/** Whether the server can be asked now. */
|
package/dist/sync.js
CHANGED
|
@@ -47,6 +47,8 @@ export const POLL_MS = 4000;
|
|
|
47
47
|
const BACKOFF_MAX_MS = 30_000;
|
|
48
48
|
/** Storage gets this long to load, or to keep an intent, before the page goes on without it. */
|
|
49
49
|
export const STORAGE_DEADLINE_MS = 3000;
|
|
50
|
+
/** Commands per push unless the spec says otherwise; the rest wait for the next push. */
|
|
51
|
+
export const PUSH_MAX = 50;
|
|
50
52
|
/** Settled commands stay drawn this long. */
|
|
51
53
|
export const DONE_KEEP_MS = 60_000;
|
|
52
54
|
/** Whether the server can be asked now. */
|
|
@@ -114,6 +116,10 @@ function failedConnection(connection, failure) {
|
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
export function createSync(spec) {
|
|
119
|
+
const pushMax = spec.pushMax ?? PUSH_MAX;
|
|
120
|
+
if (!Number.isSafeInteger(pushMax) || pushMax < 1) {
|
|
121
|
+
throw new Error(`sync: pushMax must be a positive integer, got ${pushMax}`);
|
|
122
|
+
}
|
|
117
123
|
const emptyReplica = () => ({
|
|
118
124
|
dataset: null,
|
|
119
125
|
revision: 0,
|
|
@@ -212,12 +218,14 @@ export function createSync(spec) {
|
|
|
212
218
|
const first = next.commands.find((each) => each.state.kind === "queued" || each.state.kind === "unknown");
|
|
213
219
|
if (first !== undefined) {
|
|
214
220
|
// One push per client, dataset and tapped-against revision: the server judges each by what it was made on.
|
|
221
|
+
// At most `pushMax`, oldest first; the rest go in the next push.
|
|
215
222
|
const tapped = (each) => each.since ?? next.replica.revision;
|
|
216
223
|
const ids = new Set(next.commands
|
|
217
224
|
.filter((each) => each.client === first.client &&
|
|
218
225
|
each.dataset === first.dataset &&
|
|
219
226
|
tapped(each) === tapped(first) &&
|
|
220
227
|
(each.state.kind === "queued" || each.state.kind === "unknown"))
|
|
228
|
+
.slice(0, pushMax)
|
|
221
229
|
.map((each) => each.id));
|
|
222
230
|
const mine = (each) => each.client === first.client && ids.has(each.id);
|
|
223
231
|
out.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slopkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Opinionated standards and libraries for fast, local-first personal software: state machines, exhaustive state tables, sync, a command ledger, jobs, a guarded server, and the conformance suite that proves them",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"conformance",
|