@joystyk/rooms 0.0.0-stage → 0.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/NOTICE +36 -0
- package/README.md +149 -2
- package/dist/canonical.d.ts +10 -0
- package/dist/canonical.js +30 -0
- package/dist/canonical.js.map +1 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +1879 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/names.d.ts +19 -0
- package/dist/names.js +28 -0
- package/dist/names.js.map +1 -0
- package/dist/node/host.d.ts +67 -0
- package/dist/node/host.js +323 -0
- package/dist/node/host.js.map +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +7 -0
- package/dist/node/jobs.d.ts +48 -0
- package/dist/node/jobs.js +176 -0
- package/dist/node/jobs.js.map +1 -0
- package/dist/node/registry.d.ts +32 -0
- package/dist/node/registry.js +43 -0
- package/dist/node/registry.js.map +1 -0
- package/dist/node/server.d.ts +59 -0
- package/dist/node/server.js +577 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/store.d.ts +53 -0
- package/dist/node/store.js +245 -0
- package/dist/node/store.js.map +1 -0
- package/dist/node/support.d.ts +67 -0
- package/dist/node/support.js +126 -0
- package/dist/node/support.js.map +1 -0
- package/dist/policy.d.ts +15 -0
- package/dist/policy.js +41 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol/index.d.ts +43 -0
- package/dist/protocol/index.js +48 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/schemas.d.ts +792 -0
- package/dist/protocol/schemas.js +332 -0
- package/dist/protocol/schemas.js.map +1 -0
- package/dist/protocol/types.d.ts +279 -0
- package/dist/protocol/types.js +1 -0
- package/dist/record.d.ts +173 -0
- package/dist/record.js +181 -0
- package/dist/record.js.map +1 -0
- package/dist/types.d.ts +374 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @joystyk/rooms
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bb095c7: A heavy AI the job pool couldn't answer is asked again with backoff instead of playing the
|
|
8
|
+
timeout move. `ai-chosen` without a command now carries the pool's `failure`, `LiveRoom` gains
|
|
9
|
+
`aiRetry`, and `CoreLimits` gains `aiRetryMs`, `aiRetryMaxMs` and `aiAttempts`. The worker pool
|
|
10
|
+
runs each worker's jobs one at a time, counts a job's timeout from when it starts, and reports
|
|
11
|
+
`busy`, `lost` and `crashed` separately (`PoolOutcome`).
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- bb095c7: Share canonical IPv4 and IPv6 /64 rate-limit keys across Node services. Keep room
|
|
16
|
+
initialization diagnostics on the server behind a client-safe reference, and expire old
|
|
17
|
+
message-rate strikes. Extend hidden-information conformance checks with transition-aware
|
|
18
|
+
perturbations so private events remain testable when recipients gain cards.
|
|
19
|
+
- Updated dependencies [bb095c7]
|
|
20
|
+
- Updated dependencies [bb095c7]
|
|
21
|
+
- Updated dependencies [bb095c7]
|
|
22
|
+
- @joystyk/contracts@0.2.0
|
|
23
|
+
- @joystyk/ops@0.2.0
|
|
24
|
+
- @joystyk/identity@0.2.0
|
|
25
|
+
- @joystyk/kit@0.2.0
|
|
26
|
+
|
|
27
|
+
## 0.1.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- Prepare the first compiled library release: ESM and API declarations for every public entry point, runtime assets, and checks against installed npm tarballs. Games and applications remain private.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- Updated dependencies
|
|
36
|
+
- @joystyk/contracts@0.1.0
|
|
37
|
+
- @joystyk/kit@0.1.0
|
|
38
|
+
- @joystyk/identity@0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joystyk contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Third-party material in the Joystyk library release
|
|
2
|
+
|
|
3
|
+
This notice preserves third-party attribution. Original Joystyk code in the 17
|
|
4
|
+
platform libraries under packages/ uses the MIT license in packages/LICENSE.
|
|
5
|
+
That choice does not license the private applications, games or other repository
|
|
6
|
+
material, and does not replace the third-party terms below.
|
|
7
|
+
|
|
8
|
+
@joystyk/contracts
|
|
9
|
+
------------------
|
|
10
|
+
Standard Schema v1 type definitions from @standard-schema/spec 1.1.0:
|
|
11
|
+
https://github.com/standard-schema/standard-schema
|
|
12
|
+
https://standardschema.dev
|
|
13
|
+
Copyright (c) 2024 Colin McDonnell. MIT License.
|
|
14
|
+
The complete license and source/change notes accompany the distributed
|
|
15
|
+
dist/standard-schema.d.ts definitions. Changes: individually exported declarations
|
|
16
|
+
and formatting; the declarations themselves are unchanged.
|
|
17
|
+
|
|
18
|
+
@joystyk/identity and @joystyk/content
|
|
19
|
+
-----------------------------------
|
|
20
|
+
Word blocklists derived from List of Dirty, Naughty, Obscene, and Otherwise Bad
|
|
21
|
+
Words, copyright Shutterstock, Inc. and contributors:
|
|
22
|
+
https://github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words
|
|
23
|
+
Licensed under Creative Commons Attribution 4.0 International:
|
|
24
|
+
https://creativecommons.org/licenses/by/4.0/
|
|
25
|
+
Identity uses the English list at commit
|
|
26
|
+
5faf2ba42d7b1c0977169ec3611df25a3c08eb13. Changes include reducing or dropping
|
|
27
|
+
phrases, dropping words with everyday meanings, substituting stems for inflections
|
|
28
|
+
and adding entries. Identity's README and content's lists/blocklist.txt retain
|
|
29
|
+
their source-specific provenance and changes.
|
|
30
|
+
|
|
31
|
+
@joystyk/content
|
|
32
|
+
---------------
|
|
33
|
+
SCOWL-derived common, vocabulary and ranked word lists use SCOWL 2020.12.07.
|
|
34
|
+
Their complete source licenses and attribution accompany the package in
|
|
35
|
+
lists/SCOWL-COPYRIGHT. Word-list transformations and build provenance are described
|
|
36
|
+
in the package README. Preserve that copyright file when redistributing the lists.
|
package/README.md
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @joystyk/rooms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The room server for every server-authoritative multiplayer game: a pure room core, the
|
|
4
|
+
versioned wire protocol, and a Node adapter that runs the core on WebSockets, SQLite, one
|
|
5
|
+
timer per room and a `worker_threads` job pool. The design is
|
|
6
|
+
room-server.md (Joystyk host reference); its "Changes from implementation"
|
|
7
|
+
section lists where this package departs from the first draft.
|
|
8
|
+
|
|
9
|
+
## npm package
|
|
10
|
+
|
|
11
|
+
The first `0.1.0` release is being prepared; it has not been published. Once available:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @joystyk/rooms
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node consumers require Node 24.15 or later. Browser entry points require a modern
|
|
18
|
+
browser and should be loaded in the browser.
|
|
19
|
+
|
|
20
|
+
| Import | Format |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `@joystyk/rooms` | ES module and TypeScript declarations |
|
|
23
|
+
| `@joystyk/rooms/protocol` | ES module and TypeScript declarations |
|
|
24
|
+
| `@joystyk/rooms/node` | ES module and TypeScript declarations |
|
|
25
|
+
|
|
26
|
+
Only the entries listed above are public API. Installed packages contain compiled ESM,
|
|
27
|
+
type declarations and runtime assets. Repository-relative links describe the Joystyk
|
|
28
|
+
workspace and are not needed to use the npm package.
|
|
29
|
+
|
|
30
|
+
| Entry | Kind | Contents |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `@joystyk/rooms` | Pure | `createRoomCore`, the room types, the record schema and migrations, `policyFromManifest`, `admitName` |
|
|
33
|
+
| `@joystyk/rooms/protocol` | Pure (`zod/mini`) | Wire schemas and types, `PROTOCOL_VERSION`, close and rejection codes |
|
|
34
|
+
| `@joystyk/rooms/node` | Node | `createRoomServer`, `RoomHost`, `openSqliteStore`, `workerJobPool`, `createEngineRegistry` |
|
|
35
|
+
|
|
36
|
+
The `session` package, and the `p2p` package that comes after launch (host policy), may import
|
|
37
|
+
`@joystyk/rooms/protocol` and nothing else from here. The service that deploys all of this
|
|
38
|
+
is `apps/rooms` (`@joystyk/rooms-server`).
|
|
39
|
+
|
|
40
|
+
## The core
|
|
41
|
+
|
|
42
|
+
A room is a value. Everything that happens to it goes through one function:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createRoomCore } from '@joystyk/rooms';
|
|
46
|
+
|
|
47
|
+
const core = createRoomCore({ engine, pin: { id: 'hearts', build: '1.4.0', rulesVersion: 3, saveVersion: 2 }, rng: sfc32 });
|
|
48
|
+
const created = core.create({ code, policy, config, seeds, creator: { name, tokenHash } }, now);
|
|
49
|
+
let room = core.load(created.value);
|
|
50
|
+
const { room: next, effects } = core.step(room, { type: 'resume' }, now);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`step(room, command, now)` reads no clock, randomness, storage or network. Commands are
|
|
54
|
+
`join`, `leave`, `act`, `control` (ready, settings, fill-with-ai, remove-seat, start,
|
|
55
|
+
offer, accept, decline, withdraw, leave-seat, resign), `resync`, `timer-fired`, `job-completed`, `ai-chosen`,
|
|
56
|
+
`resume` and `close`. Effects are data for the adapter: `persist` (always first, at most
|
|
57
|
+
once), `welcome`, `send`, `drop`, `alarm`, `start-job`, `start-ai` and `log`.
|
|
58
|
+
|
|
59
|
+
What the core guarantees, each covered by the property tests in `core.property.test.ts`:
|
|
60
|
+
|
|
61
|
+
- **Deterministic.** The same room, command and `now` give deeply equal steps.
|
|
62
|
+
- **Persist before broadcast.** If the record changed, the first effect saves it; a
|
|
63
|
+
rejection changes nothing durable.
|
|
64
|
+
- **Per-seat staleness.** An action is stale only if the seat's `decisionKey` changed
|
|
65
|
+
after the revision the client saw, so simultaneous moves aren't refused.
|
|
66
|
+
- **Receipts.** A retried action ID with the same payload gets its original receipt, even
|
|
67
|
+
after a restart; the same ID with a different payload is refused.
|
|
68
|
+
- **Clocks as data.** Deadlines live in the record; whatever is due at `now` applies before
|
|
69
|
+
the command. A timer that fires early, late or twice can't apply a stale move. A room
|
|
70
|
+
runs a per-move clock or a time bank with increment (`policy.bank`); banks are charged
|
|
71
|
+
from the record alone, never for a playback hold.
|
|
72
|
+
- **The AI's stream.** Each AI decision gets a child forked from `aiRng` with the kit's
|
|
73
|
+
`forkRng`; the stream moves only when an AI move is accepted. A heavy AI
|
|
74
|
+
(`heavyAi: true`) never chooses inside a step: the core emits `start-ai`, the adapter runs
|
|
75
|
+
it on the pool, and `ai-chosen` applies only if the seat still waits on that decision.
|
|
76
|
+
- **No leaks.** Each seat receives `viewFor(state, seat)` and `eventFor(event, seat)`;
|
|
77
|
+
spectators get the `null` view.
|
|
78
|
+
- **Paused when empty.** With no player connected, no AI move, clock, offer or absence
|
|
79
|
+
policy runs. The pause is recorded (`match.pausedAt`), and when a player returns every
|
|
80
|
+
deadline moves on by its length, so no clock or bank is charged for it (host policy).
|
|
81
|
+
- **Offers.** Any seated player offers a rematch, or to end a match early; the others
|
|
82
|
+
accept or decline, and the proposer may withdraw. A rematch starts once every seated
|
|
83
|
+
player has accepted (computer seats accept on their own); an early end needs every other
|
|
84
|
+
connected player, or a minute with nobody declining, and leaves no result (host policy).
|
|
85
|
+
- **Pinned versions.** A match keeps its game build and rules version; a rematch starts on
|
|
86
|
+
the newest engine.
|
|
87
|
+
- **Filtered names.** The creator's name and every name that claims a seat pass
|
|
88
|
+
`checkDisplayName` from `@joystyk/identity/names`, the only part of identity this package
|
|
89
|
+
imports. A refused name becomes a generated one, seeded by the room's code, epoch, revision
|
|
90
|
+
and seat, so the step stays deterministic; the player gets a `name-replaced` notice that
|
|
91
|
+
says why, and `POST /api/rooms` answers with the seat's `name` and the same `notice`.
|
|
92
|
+
|
|
93
|
+
The core takes the engine's seeded generator as an argument (`rng: (state) => Rng`; the
|
|
94
|
+
service passes the kit's `restoreRng`). The adapter supplies fresh seeds and token hashes; no
|
|
95
|
+
secret ever enters the core.
|
|
96
|
+
|
|
97
|
+
## The Node adapter
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createEngineRegistry, createRoomServer, openSqliteStore, workerJobPool } from '@joystyk/rooms/node';
|
|
101
|
+
|
|
102
|
+
const store = await openSqliteStore('/data/rooms.sqlite');
|
|
103
|
+
const registry = createEngineRegistry([{ pin, engine, policy, jobs: { throw: new URL('./jobs/throw.worker.ts', import.meta.url) } }], sfc32);
|
|
104
|
+
const server = createRoomServer({ store, registry, seeds, pool: workerJobPool({ workers: 1, module: registry.jobModule }), publicOrigin });
|
|
105
|
+
await server.listen(8787);
|
|
106
|
+
process.once('SIGTERM', () => void server.drain());
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- **HTTP.** `POST /api/rooms` creates a room and returns its code with the creator's seat
|
|
110
|
+
token; `GET /api/rooms/:code` describes it; `GET /api/rooms/games` lists the games the
|
|
111
|
+
registry hosts and the protocol versions (`HostedGames`), which the launcher reads before
|
|
112
|
+
offering Online with friends; `/healthz` and `/readyz` serve Compose.
|
|
113
|
+
- **WebSocket** at `/ws`. Messages are decoded with the protocol schemas, then the game's
|
|
114
|
+
own command schema. The server answers each connection in the protocol version it joined
|
|
115
|
+
with.
|
|
116
|
+
- **Persistence.** `node:sqlite` in WAL mode. Each persist is one transaction that updates
|
|
117
|
+
the row only if its `writes` count matches, so a second writer gets `conflict` and
|
|
118
|
+
unloads. A failed write keeps the old state, sends nothing and retries. Schema migrations
|
|
119
|
+
run at start-up, a newer database is refused, and corrupt rows move to `room_archive`.
|
|
120
|
+
- **Limits.** Payload size, a message-rate token bucket, rooms per address per hour,
|
|
121
|
+
unknown-code joins per address, connections per address and in total, rooms in memory,
|
|
122
|
+
spectators per room, outgoing buffer size and a WebSocket ping heartbeat. The client address
|
|
123
|
+
is read only from a trusted proxy (an IP or CIDR range).
|
|
124
|
+
- **Jobs.** A game's `jobs/<kind>.worker.ts` default-exports `(payload) => JobOutcome`; its
|
|
125
|
+
`jobs/ai.worker.ts` (heavy AI) maps `{ view, seat, level, rng }` to a command. The pool
|
|
126
|
+
bounds queued work, times out stuck jobs from when they start (a heavy AI after
|
|
127
|
+
`aiTimeoutMs`) and restarts the worker. A heavy AI the pool couldn't answer is asked again
|
|
128
|
+
with backoff.
|
|
129
|
+
- **Drain and resume.** `drain()` refuses new work, waits for running jobs, sends
|
|
130
|
+
`closing` and closes with 1012. On start, rooms with an outstanding job load at once so
|
|
131
|
+
the job re-runs; other rooms load when someone joins.
|
|
132
|
+
- **Logs.** One JSON object per line, never with tokens, names, addresses or game state.
|
|
133
|
+
|
|
134
|
+
## Tests
|
|
135
|
+
|
|
136
|
+
| File | What it proves |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `core.test.ts` | Seats and display names, receipts, stale and future revisions, controls, AI handover, the host, clocks, jobs, failures |
|
|
139
|
+
| `core.clocks-ai.test.ts` | Time banks, the AI stream, heavy AI |
|
|
140
|
+
| `core.pause.test.ts` | A paused table: clocks stop and resume, banks are charged only for live time, a pause survives a restart |
|
|
141
|
+
| `core.offers.test.ts` | Rematch and end-match offers: offer, accept, decline, withdraw, computer seats accepting, the one-minute end, a pause during it |
|
|
142
|
+
| `core.property.test.ts` | The invariants above, under random command sequences for five set-ups: hidden hands, AI handover, heavy AI, a time bank and a simultaneous phase |
|
|
143
|
+
| `protocol/protocol.test.ts` | Message schemas, and that every message the core sends decodes |
|
|
144
|
+
| `node/store.test.ts` | The SQLite store on a real file: conflicts, migrations, quarantine, backups |
|
|
145
|
+
| `node/host.test.ts` | Restart and resume, a failing store, two writers, jobs that survive a restart |
|
|
146
|
+
| `node/server.test.ts` | Two real WebSocket clients: join, play, reconnect, newest tab wins, spectators, a replaced name, limits, drain |
|
|
147
|
+
| `node/jobs.test.ts` | The `worker_threads` pool with real worker modules |
|
|
148
|
+
|
|
149
|
+
The test games live in `src/__tests__/fixtures.ts`: Nim (turn-based), Peek (hidden hands,
|
|
150
|
+
a job, an AI) and Pass (a simultaneous phase whose decision key ignores who has passed).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Json } from '@joystyk/contracts';
|
|
2
|
+
/** JSON with object keys sorted, so equal values always produce the same text. */
|
|
3
|
+
export declare const canonicalJson: (value: Json) => string;
|
|
4
|
+
/**
|
|
5
|
+
* A 64-bit hash of a string as 16 hex digits (two independent 32-bit FNV-1a style lanes).
|
|
6
|
+
* Not cryptographic: it only tells a retried action from a different one with the same ID.
|
|
7
|
+
*/
|
|
8
|
+
export declare const hashText: (text: string) => string;
|
|
9
|
+
/** The receipt fingerprint of an action: canonical JSON of what the client sent, hashed. */
|
|
10
|
+
export declare const fingerprint: (value: Json) => string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/canonical.ts
|
|
2
|
+
/** JSON with object keys sorted, so equal values always produce the same text. */
|
|
3
|
+
var canonicalJson = (value) => {
|
|
4
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
5
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
6
|
+
const object = value;
|
|
7
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A 64-bit hash of a string as 16 hex digits (two independent 32-bit FNV-1a style lanes).
|
|
11
|
+
* Not cryptographic: it only tells a retried action from a different one with the same ID.
|
|
12
|
+
*/
|
|
13
|
+
var hashText = (text) => {
|
|
14
|
+
let a = 2166136261;
|
|
15
|
+
let b = 16777619 ^ text.length;
|
|
16
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
17
|
+
const code = text.charCodeAt(i);
|
|
18
|
+
a = Math.imul(a ^ code, 16777619);
|
|
19
|
+
b = Math.imul(b ^ code, 1540483477);
|
|
20
|
+
b ^= b >>> 13;
|
|
21
|
+
}
|
|
22
|
+
const hex = (n) => (n >>> 0).toString(16).padStart(8, "0");
|
|
23
|
+
return hex(a) + hex(b);
|
|
24
|
+
};
|
|
25
|
+
/** The receipt fingerprint of an action: canonical JSON of what the client sent, hashed. */
|
|
26
|
+
var fingerprint = (value) => hashText(canonicalJson(value));
|
|
27
|
+
//#endregion
|
|
28
|
+
export { canonicalJson, fingerprint, hashText };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=canonical.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.js","names":[],"sources":["../src/canonical.ts"],"sourcesContent":["// Canonical JSON and a small pure hash, for receipt fingerprints and change detection. Pure\n// and deterministic in every JavaScript engine: integer arithmetic through Math.imul only.\nimport type { Json } from '@joystyk/contracts';\n\n/** JSON with object keys sorted, so equal values always produce the same text. */\nexport const canonicalJson = (value: Json): string => {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map((item: Json) => canonicalJson(item)).join(',')}]`;\n const object = value as { readonly [key: string]: Json };\n const keys = Object.keys(object).sort();\n return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key] as Json)}`).join(',')}}`;\n};\n\n/**\n * A 64-bit hash of a string as 16 hex digits (two independent 32-bit FNV-1a style lanes).\n * Not cryptographic: it only tells a retried action from a different one with the same ID.\n */\nexport const hashText = (text: string): string => {\n let a = 0x811c9dc5;\n let b = 0x01000193 ^ text.length;\n for (let i = 0; i < text.length; i += 1) {\n const code = text.charCodeAt(i);\n a = Math.imul(a ^ code, 0x01000193);\n b = Math.imul(b ^ code, 0x5bd1e995);\n b ^= b >>> 13;\n }\n const hex = (n: number): string => (n >>> 0).toString(16).padStart(8, '0');\n return hex(a) + hex(b);\n};\n\n/** The receipt fingerprint of an action: canonical JSON of what the client sent, hashed. */\nexport const fingerprint = (value: Json): string => hashText(canonicalJson(value));\n"],"mappings":";;AAKA,IAAa,iBAAiB,UAAwB;CACpD,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO,KAAK,UAAU,KAAK;CAC5E,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,IAAI,MAAM,KAAK,SAAe,cAAc,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;CAC9F,MAAM,SAAS;CAEf,OAAO,IADM,OAAO,KAAK,MAAM,CAAC,CAAC,KACtB,CAAA,CAAK,KAAK,QAAQ,GAAG,KAAK,UAAU,GAAG,EAAE,GAAG,cAAc,OAAO,IAAY,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE;AACzG;;;;;AAMA,IAAa,YAAY,SAAyB;CAChD,IAAI,IAAI;CACR,IAAI,IAAI,WAAa,KAAK;CAC1B,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;EACvC,MAAM,OAAO,KAAK,WAAW,CAAC;EAC9B,IAAI,KAAK,KAAK,IAAI,MAAM,QAAU;EAClC,IAAI,KAAK,KAAK,IAAI,MAAM,UAAU;EAClC,KAAK,MAAM;CACb;CACA,MAAM,OAAO,OAAuB,MAAM,EAAA,CAAG,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CACzE,OAAO,IAAI,CAAC,IAAI,IAAI,CAAC;AACvB;;AAGA,IAAa,eAAe,UAAwB,SAAS,cAAc,KAAK,CAAC"}
|
package/dist/core.d.ts
ADDED