@fairfox/polly 0.69.0 → 0.71.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/dist/src/client/index.js +17 -20
- package/dist/src/client/index.js.map +4 -4
- package/dist/src/mesh.js +462 -154
- package/dist/src/mesh.js.map +9 -7
- package/dist/src/peer.js +153 -9
- package/dist/src/peer.js.map +5 -4
- package/dist/src/polly-ui/markdown.js +3 -3
- package/dist/src/polly-ui/markdown.js.map +2 -2
- package/dist/src/shared/lib/mesh-client.d.ts +29 -0
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +129 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +90 -3
- package/dist/src/shared/lib/revocation-summary.d.ts +54 -0
- package/dist/tools/test/src/e2e-mesh/console-allowlist.d.ts +31 -0
- package/dist/tools/test/src/e2e-mesh/index.d.ts +27 -0
- package/dist/tools/test/src/e2e-mesh/index.js +1183 -0
- package/dist/tools/test/src/e2e-mesh/index.js.map +22 -0
- package/dist/tools/test/src/e2e-mesh/keys.d.ts +55 -0
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +98 -0
- package/dist/tools/test/src/e2e-mesh/mesh-assertions.d.ts +53 -0
- package/dist/tools/test/src/e2e-mesh/serve-consumer.d.ts +32 -0
- package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +38 -0
- package/dist/tools/test/src/e2e-mesh/with-relay.d.ts +53 -0
- package/dist/tools/test/src/visual/index.js +24 -24
- package/dist/tools/test/src/visual/index.js.map +2 -2
- package/dist/tools/verify/src/cli.js +361 -22
- package/dist/tools/verify/src/cli.js.map +6 -6
- package/dist/tools/verify/src/config.d.ts +26 -1
- package/dist/tools/verify/src/config.js +9 -1
- package/dist/tools/verify/src/config.js.map +4 -4
- package/dist/tools/verify/src/primitives/index.d.ts +30 -0
- package/dist/tools/visualize/src/cli.js +43 -1
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +11 -8
- package/LICENSE +0 -21
- package/README.md +0 -362
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fairfox/polly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Multi-execution-context framework with reactive state and cross-context messaging for Chrome extensions, PWAs, and worker-based applications",
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
"import": "./dist/tools/test/src/visual/index.js",
|
|
71
71
|
"types": "./dist/tools/test/src/visual/index.d.ts"
|
|
72
72
|
},
|
|
73
|
+
"./test/e2e-mesh": {
|
|
74
|
+
"import": "./dist/tools/test/src/e2e-mesh/index.js",
|
|
75
|
+
"types": "./dist/tools/test/src/e2e-mesh/index.d.ts"
|
|
76
|
+
},
|
|
73
77
|
"./peer": {
|
|
74
78
|
"import": "./dist/src/peer.js",
|
|
75
79
|
"types": "./dist/src/peer.d.ts"
|
|
@@ -125,8 +129,8 @@
|
|
|
125
129
|
"build:prod": "bun run build.ts --prod",
|
|
126
130
|
"build:lib": "bun scripts/build-polly-ui-registry.ts && bun run build-lib.ts",
|
|
127
131
|
"build:registry": "bun scripts/build-polly-ui-registry.ts",
|
|
128
|
-
"build:full-featured": "bun run scripts/build-user-extension.ts examples/full-featured",
|
|
129
|
-
"build:full-featured:prod": "bun run scripts/build-user-extension.ts examples/full-featured --prod",
|
|
132
|
+
"build:full-featured": "bun run scripts/build-user-extension.ts ../../examples/full-featured",
|
|
133
|
+
"build:full-featured:prod": "bun run scripts/build-user-extension.ts ../../examples/full-featured --prod",
|
|
130
134
|
"typecheck": "bunx tsc --noEmit && bun run --cwd tests typecheck",
|
|
131
135
|
"typecheck:tests": "bun run --cwd tests typecheck",
|
|
132
136
|
"lint": "biome check .",
|
|
@@ -134,12 +138,10 @@
|
|
|
134
138
|
"format": "biome format --write .",
|
|
135
139
|
"test": "bun run --cwd tests test",
|
|
136
140
|
"test:watch": "bun run --cwd tests test:watch",
|
|
137
|
-
"test:all": "bun run lint && bun run typecheck && bun run --cwd tests test:all
|
|
138
|
-
"test:examples": "echo '\\n=== Testing Examples ===' && bun run test:example:minimal && bun run test:example:todo-list && bun run test:example:full-featured",
|
|
139
|
-
"test:example:minimal": "echo '\\n--- Minimal Example ---' && bun run --cwd examples/minimal test:all",
|
|
140
|
-
"test:example:todo-list": "echo '\\n--- Todo List ---' && bun run --cwd examples/todo-list test:all",
|
|
141
|
-
"test:example:full-featured": "echo '\\n--- Full Featured ---' && bun run --cwd examples/full-featured test:all",
|
|
141
|
+
"test:all": "bun run lint && bun run typecheck && bun run --cwd tests test:all",
|
|
142
142
|
"test:e2e:visualize": "bun scripts/e2e-visualize.ts",
|
|
143
|
+
"test:e2e:mesh:fast": "bun scripts/e2e-mesh-offline-online-drain.ts",
|
|
144
|
+
"test:e2e:mesh": "bun scripts/e2e-mesh-offline-online-drain.ts && bun scripts/e2e-mesh-revocation-prebaked.ts && bun scripts/e2e-mesh-three-peer-convergence.ts && bun scripts/e2e-mesh-revocation-runtime.ts && bun scripts/e2e-mesh-revocation-propagation.ts && bun scripts/e2e-mesh-revocation-offline-catchup.ts && bun scripts/e2e-mesh-corrupted-state-recovery.ts",
|
|
143
145
|
"check": "bun scripts/check.ts",
|
|
144
146
|
"prepublishOnly": "bun run typecheck && bun run lint && bun run --cwd tests test && bun run build:lib",
|
|
145
147
|
"publish:public": "npm publish --access public --otp=$(pass-cli item totp --output json --item-id Rdr--oUgHp-IfyjoTB7vhMPHfNt6Oz2JpXfS1WrdQMQ9damDMPOvKiOxxpCZqp92WgqXnfY68cSXWdQ_JVMNMQ== --share-id Ixmpll6U5ioU_1P4fzUbarQNANRPU51O68xzFblTB08S0HUfw4dhQaNAv4Yv7Sud_Vf8ps4mnQUFqdvN-eTRdQ== | jq -r '.totp')"
|
|
@@ -171,6 +173,7 @@
|
|
|
171
173
|
},
|
|
172
174
|
"dependencies": {
|
|
173
175
|
"@preact/signals": "2.9.0",
|
|
176
|
+
"@preact/signals-core": "1.14.2",
|
|
174
177
|
"preact": "10.29.1",
|
|
175
178
|
"ts-morph": "28.0.0"
|
|
176
179
|
},
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Alex Jeffcott
|
|
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/README.md
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
# @fairfox/polly
|
|
2
|
-
|
|
3
|
-
Reactive state for multi-context apps — from single-process signals to peer-to-peer encrypted mesh — with formal verification.
|
|
4
|
-
|
|
5
|
-
## The pitch
|
|
6
|
-
|
|
7
|
-
Define state once. Read it anywhere. Polly keeps them in sync.
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
// src/shared/state.ts
|
|
11
|
-
import { $sharedState } from "@fairfox/polly/state";
|
|
12
|
-
|
|
13
|
-
export const counter = $sharedState("counter", 0);
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
// src/background/index.ts — service worker, extension background, Node process
|
|
18
|
-
import { createBackground } from "@fairfox/polly/background";
|
|
19
|
-
import { counter } from "../shared/state";
|
|
20
|
-
|
|
21
|
-
const bus = createBackground();
|
|
22
|
-
|
|
23
|
-
bus.on("INCREMENT", () => {
|
|
24
|
-
counter.value++;
|
|
25
|
-
return { count: counter.value };
|
|
26
|
-
});
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
// src/popup/index.tsx — browser popup, any Preact/React UI
|
|
31
|
-
import { render } from "preact";
|
|
32
|
-
import { counter } from "../shared/state";
|
|
33
|
-
|
|
34
|
-
function App() {
|
|
35
|
-
return (
|
|
36
|
-
<div>
|
|
37
|
-
<p>Count: {counter.value}</p>
|
|
38
|
-
<button onClick={() => counter.value++}>+</button>
|
|
39
|
-
</div>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
render(<App />, document.getElementById("root")!);
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
The background writes `counter`. The popup reads it. Both stay in sync — no message passing, no subscriptions to manage. State is a [Preact Signal](https://preactjs.com/guide/v10/signals/), so the UI re-renders automatically when the value changes.
|
|
47
|
-
|
|
48
|
-
## State that syncs everywhere
|
|
49
|
-
|
|
50
|
-
Modern apps run code in multiple isolated contexts: service workers, content scripts, server processes. Keeping state consistent across them means writing sync logic for every piece of data. Polly replaces that with four primitives:
|
|
51
|
-
|
|
52
|
-
| Primitive | Syncs | Persists | Use for |
|
|
53
|
-
|-----------|:-----:|:--------:|---------|
|
|
54
|
-
| `$sharedState` | yes | yes | User data, settings, auth — anything that should survive a restart and stay consistent |
|
|
55
|
-
| `$syncedState` | yes | no | Ephemeral shared state: connection status, live collaboration flags |
|
|
56
|
-
| `$persistedState` | no | yes | Per-context settings, form drafts |
|
|
57
|
-
| `$state` | no | no | Local UI state: loading spinners, modal visibility |
|
|
58
|
-
|
|
59
|
-
All four return Preact Signals. Read with `.value`, write with `.value =`.
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import { $sharedState, $syncedState, $persistedState, $state } from "@fairfox/polly/state";
|
|
63
|
-
|
|
64
|
-
const user = $sharedState("user", { name: "Guest", loggedIn: false });
|
|
65
|
-
const wsConnected = $syncedState("ws", false);
|
|
66
|
-
const draft = $persistedState("draft", "");
|
|
67
|
-
const loading = $state(false);
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
For async data, `$resource` fetches and re-fetches automatically when its dependencies change:
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
import { $resource } from "@fairfox/polly/resource";
|
|
74
|
-
|
|
75
|
-
const todos = $resource("todos", {
|
|
76
|
-
source: () => ({ userId: user.value.id }),
|
|
77
|
-
fetcher: async ({ userId }) => {
|
|
78
|
-
const res = await fetch(`/api/todos?userId=${userId}`);
|
|
79
|
-
return res.json();
|
|
80
|
-
},
|
|
81
|
-
initialValue: [],
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
todos.data; // Signal<Todo[]>
|
|
85
|
-
todos.status; // Signal<"idle" | "loading" | "success" | "error">
|
|
86
|
-
todos.refetch();
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Peer-first state (v0.21.0)
|
|
90
|
-
|
|
91
|
-
The four primitives above keep state consistent inside a single deployment. But some applications need state that survives the server going away, or state that the server never sees at all. Polly now offers three resilience tiers, each a distinct primitive family:
|
|
92
|
-
|
|
93
|
-
| Tier | Primitive | Server's role | Resilience |
|
|
94
|
-
|------|-----------|---------------|------------|
|
|
95
|
-
| Weakest | `$sharedState` | Source of truth | Server backups |
|
|
96
|
-
| Middle | `$peerState` | Full peer on the data path | Any device can rehydrate the server |
|
|
97
|
-
| Strongest | `$meshState` | Not on the data path | App works with zero server uptime |
|
|
98
|
-
|
|
99
|
-
**`$peerState`** — every device holds a full [Automerge](https://automerge.org/) CRDT replica. The server holds one too, so cron and HTTP handlers can read and mutate documents. If the server loses its storage, any reconnecting client repopulates it through the normal sync protocol.
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
import { createPeerStateClient, configurePeerState, $peerState } from "@fairfox/polly/peer";
|
|
103
|
-
|
|
104
|
-
const client = createPeerStateClient({ url: "wss://yourapp.com/polly/peer" });
|
|
105
|
-
configurePeerState(client.repo);
|
|
106
|
-
|
|
107
|
-
const settings = $peerState("settings", { theme: "dark" });
|
|
108
|
-
await settings.loaded;
|
|
109
|
-
settings.value = { theme: "light" }; // syncs to every peer
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**`$meshState`** — peers exchange operations directly over WebRTC data channels, signed with Ed25519 and encrypted with XSalsa20-Poly1305. No server sees the data. A small stateless signalling server helps peers find each other; removing it does not affect running connections.
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { configureMeshState, $meshState, MeshNetworkAdapter, MeshWebRTCAdapter } from "@fairfox/polly/mesh";
|
|
116
|
-
|
|
117
|
-
const repo = new Repo({ network: [new MeshNetworkAdapter({ base: webrtcAdapter, keyring })] });
|
|
118
|
-
configureMeshState(repo);
|
|
119
|
-
|
|
120
|
-
const notes = $meshState("notes", { entries: [] });
|
|
121
|
-
// Operations flow peer-to-peer, signed and encrypted
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
First-time key exchange between devices uses a pairing token displayed as a QR code. Compromised devices are revoked via signed revocation records that propagate to every peer.
|
|
125
|
-
|
|
126
|
-
The three tiers coexist in one application — public settings in `$sharedState`, collaborative documents in `$peerState`, private notes in `$meshState`. See [docs/STATE.md](docs/STATE.md) for the full decision tree and [docs/RFC-041-choosing.md](docs/RFC-041-choosing.md) for the design rationale.
|
|
127
|
-
|
|
128
|
-
### Node and Bun are first-class mesh peers
|
|
129
|
-
|
|
130
|
-
Archival cron, LLM proxies, admin CLIs, headless bridges — every always-on participant gets the same state primitives as the browser, without monkey-patching globals or writing bespoke transport wiring. Polly ships a factory that accepts injectable transport and storage:
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
import { createMeshClient, $meshState } from "@fairfox/polly/mesh";
|
|
134
|
-
import { bootstrapCliKeyring, fileKeyringStorage } from "@fairfox/polly/mesh/node";
|
|
135
|
-
import { RTCPeerConnection } from "werift"; // or '@roamhq/wrtc'
|
|
136
|
-
|
|
137
|
-
const storage = fileKeyringStorage("~/.fairfox/keyring.json");
|
|
138
|
-
const keyring = await bootstrapCliKeyring({ storage }); // first run prompts for a pairing token
|
|
139
|
-
|
|
140
|
-
const client = await createMeshClient({
|
|
141
|
-
signaling: { url: "wss://example.com/polly/signaling", peerId: "cli-a1b2" },
|
|
142
|
-
rtc: { RTCPeerConnection },
|
|
143
|
-
keyring,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const doc = $meshState("agenda", { items: [] });
|
|
147
|
-
await doc.loaded;
|
|
148
|
-
await client.close();
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
`createMeshClient` is runtime-agnostic — in a browser the `rtc` option is optional because `globalThis.RTCPeerConnection` exists. The `@fairfox/polly/mesh/node` subpath adds filesystem-backed keyring storage, atomic writes with `0600` permissions, and the stdin bootstrap flow. Neither `werift` nor `@roamhq/wrtc` is bundled; both are declared as optional peer dependencies. Pick the one that fits your deployment — `werift` installs cleanly anywhere (pure TypeScript, no native deps), `@roamhq/wrtc` is faster but needs prebuilt binaries for your platform.
|
|
152
|
-
|
|
153
|
-
### Blob storage for large files
|
|
154
|
-
|
|
155
|
-
CRDT documents shouldn't carry binary payloads — the op history grows with every sync. Polly ships a content-addressed blob store that transfers files peer-to-peer over the same WebRTC channels as `$meshState`, with no server storage. Documents hold lightweight `BlobRef` values; the bytes live in a local IndexedDB cache and move between peers in 64 KiB chunks.
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
import { createBlobStore, createBlobRef } from "@fairfox/polly/mesh";
|
|
159
|
-
|
|
160
|
-
const blobs = createBlobStore(webrtcAdapter, { encrypt: { key: docKey } });
|
|
161
|
-
|
|
162
|
-
// Sender
|
|
163
|
-
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
164
|
-
const ref = await createBlobRef({ bytes, filename: file.name, mimeType: file.type });
|
|
165
|
-
await blobs.put(ref, bytes); // caches locally, announces to peers
|
|
166
|
-
doc.value = { ...doc.value, attachment: ref };
|
|
167
|
-
|
|
168
|
-
// Receiver (on any connected peer)
|
|
169
|
-
const received = await blobs.get(ref.hash); // fetches from peers, verifies hash
|
|
170
|
-
const url = await blobs.url(ref.hash); // object URL for <img src>
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
SHA-256 content addressing deduplicates across peers and documents. Encryption is optional — when configured, the sender encrypts once (XSalsa20-Poly1305) and chunks the ciphertext; the receiver reassembles, decrypts, and verifies the plaintext hash against the `BlobRef`. See [docs/RFC-042-blob-sync.md](docs/RFC-042-blob-sync.md) for the design.
|
|
174
|
-
|
|
175
|
-
## Verification that plugs in
|
|
176
|
-
|
|
177
|
-
A popup and a background script both write to the same state. A content script reads it mid-update. Tests miss these bugs because they depend on timing.
|
|
178
|
-
|
|
179
|
-
Polly generates [TLA+](https://lamport.azuretext.org/tla/tla.html) specifications from your existing state and handlers, then model-checks them with TLC. You don't learn a new language. You annotate what you already wrote.
|
|
180
|
-
|
|
181
|
-
### Step 1: Add contracts to handlers
|
|
182
|
-
|
|
183
|
-
`requires()` and `ensures()` are runtime no-ops. `polly verify` reads them statically.
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
import { createBackground } from "@fairfox/polly/background";
|
|
187
|
-
import { requires, ensures } from "@fairfox/polly/verify";
|
|
188
|
-
import { user, todos } from "./state";
|
|
189
|
-
|
|
190
|
-
const bus = createBackground();
|
|
191
|
-
|
|
192
|
-
bus.on("TODO_ADD", (payload: { text: string }) => {
|
|
193
|
-
requires(user.value.loggedIn === true, "Must be logged in");
|
|
194
|
-
requires(payload.text !== "", "Text must not be empty");
|
|
195
|
-
|
|
196
|
-
todos.value = [...todos.value, {
|
|
197
|
-
id: Date.now().toString(),
|
|
198
|
-
text: payload.text,
|
|
199
|
-
completed: false,
|
|
200
|
-
}];
|
|
201
|
-
|
|
202
|
-
ensures(todos.value.length > 0, "Todos must not be empty after add");
|
|
203
|
-
|
|
204
|
-
return { success: true };
|
|
205
|
-
});
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Step 2: Define state bounds
|
|
209
|
-
|
|
210
|
-
A verification config tells TLC what values to explore:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
// specs/verification.config.ts
|
|
214
|
-
import { defineVerification } from "@fairfox/polly/verify";
|
|
215
|
-
|
|
216
|
-
export const verificationConfig = defineVerification({
|
|
217
|
-
state: {
|
|
218
|
-
"user.loggedIn": { type: "boolean" },
|
|
219
|
-
"user.role": { type: "enum", values: ["guest", "user", "admin"] },
|
|
220
|
-
todos: { maxLength: 1 },
|
|
221
|
-
},
|
|
222
|
-
messages: {
|
|
223
|
-
maxInFlight: 2,
|
|
224
|
-
maxTabs: 1,
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Step 3: Run it
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
$ polly verify
|
|
233
|
-
|
|
234
|
-
Generating TLA+ specification...
|
|
235
|
-
Running TLC model checker...
|
|
236
|
-
|
|
237
|
-
Model checking complete.
|
|
238
|
-
States explored: 1,247
|
|
239
|
-
Distinct states: 312
|
|
240
|
-
No errors found.
|
|
241
|
-
|
|
242
|
-
All properties verified.
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
If a `requires()` can be violated — say, a logout races with a todo add — TLC finds the exact sequence of steps that triggers it.
|
|
246
|
-
|
|
247
|
-
For larger apps, [subsystem-scoped verification](https://github.com/AlexJeffcott/polly/tree/main/examples/todo-list) splits the state space so checking stays fast.
|
|
248
|
-
|
|
249
|
-
## Quick start
|
|
250
|
-
|
|
251
|
-
```bash
|
|
252
|
-
bun add @fairfox/polly preact @preact/signals
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
Scaffold a project:
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
polly init my-app --type=extension # or: pwa, websocket, generic
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Or start from one of the examples:
|
|
262
|
-
|
|
263
|
-
```bash
|
|
264
|
-
git clone https://github.com/AlexJeffcott/polly.git
|
|
265
|
-
cd polly/examples/minimal
|
|
266
|
-
bun install && bun run dev
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
## Examples
|
|
270
|
-
|
|
271
|
-
| Example | What it demonstrates |
|
|
272
|
-
|---------|---------------------|
|
|
273
|
-
| [minimal](examples/minimal) | Counter with `$sharedState` — the simplest possible Polly app |
|
|
274
|
-
| [todo-list](examples/todo-list) | CRUD with `requires()`/`ensures()`, subsystem-scoped verification |
|
|
275
|
-
| [full-featured](examples/full-featured) | Production Chrome extension with all framework features |
|
|
276
|
-
| [elysia-todo-app](examples/elysia-todo-app) | Full-stack web app with Elysia + Bun, offline-first |
|
|
277
|
-
| [webrtc-p2p-chat](examples/webrtc-p2p-chat) | Peer-to-peer chat over WebRTC data channels |
|
|
278
|
-
| [team-task-manager](examples/team-task-manager) | Role-based collaborative tasks with `$constraints()` and verified urgent-task counts |
|
|
279
|
-
|
|
280
|
-
## CLI
|
|
281
|
-
|
|
282
|
-
```
|
|
283
|
-
polly init [name] [--type=TYPE] Scaffold a new project (extension | pwa | websocket | generic)
|
|
284
|
-
polly build [--prod] Build for development or production
|
|
285
|
-
polly dev Build with watch mode
|
|
286
|
-
polly check Run all checks (typecheck, lint, test, build)
|
|
287
|
-
polly typecheck Type-check your code
|
|
288
|
-
polly lint [--fix] Lint (and optionally auto-fix)
|
|
289
|
-
polly format Format your code
|
|
290
|
-
polly test [args] Run unit tests (bun test)
|
|
291
|
-
polly test:browser [dir] Run *.browser.{ts,tsx} in Puppeteer
|
|
292
|
-
polly verify Run formal verification (TLA+/TLC)
|
|
293
|
-
polly visualize Generate architecture diagrams (Structurizr DSL)
|
|
294
|
-
polly quality [args] Run conformance checks (no-as-casting, boundaries, secrets,
|
|
295
|
-
server-imports, forbidden-deps, banners, marketing, ...)
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
## Quality tooling
|
|
299
|
-
|
|
300
|
-
Polly ships conformance checks and a browser test harness that consuming applications can adopt directly.
|
|
301
|
-
|
|
302
|
-
**No-as-casting check.** Bans TypeScript `as` type assertions codebase-wide (only `as const` and the explicit escape hatch `as unknown as` are allowed). Violations include pattern-specific fix advice. Run as a CLI:
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
polly quality [--root <dir>] [--exclude-packages <names>] [--exclude-files <names>]
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Or import it programmatically for integration into custom check scripts:
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
import { checkNoAsCasting } from "@fairfox/polly/quality";
|
|
312
|
-
|
|
313
|
-
const result = await checkNoAsCasting({ rootDir: process.cwd() });
|
|
314
|
-
if (result.violations.length > 0) {
|
|
315
|
-
result.print();
|
|
316
|
-
process.exit(1);
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
**Browser test harness.** A Puppeteer-based harness for testing browser-only code (WebRTC adapters, Preact components, anything needing real DOM). Bundles each test file with Bun.build, serves on an ephemeral port, and collects results:
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
import { describe, test, expect, done, flush, cleanup } from "@fairfox/polly/test/browser";
|
|
324
|
-
|
|
325
|
-
describe("my component", () => {
|
|
326
|
-
test("renders", async () => {
|
|
327
|
-
render(<MyComponent />, app);
|
|
328
|
-
await flush();
|
|
329
|
-
expect(app.querySelector("h1")).toHaveTextContent("Hello");
|
|
330
|
-
cleanup(app);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
done();
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Run with `bun tools/test/src/browser/run.ts tests/browser`.
|
|
338
|
-
|
|
339
|
-
## Use with Claude Code
|
|
340
|
-
|
|
341
|
-
This repo ships with a [Claude Code skill](.claude/skills) that covers the full Polly API — state primitives, peer-first and mesh state, verification, quality tooling, and the browser test harness. Install it in your project so Claude can help you integrate Polly with full context:
|
|
342
|
-
|
|
343
|
-
```bash
|
|
344
|
-
# From your project directory
|
|
345
|
-
claude install-skill /path/to/polly/.claude/skills
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Then ask Claude things like:
|
|
349
|
-
- "How would Polly fit into this project?"
|
|
350
|
-
- "Add verification to my handlers"
|
|
351
|
-
- "Set up $peerState with an Elysia server"
|
|
352
|
-
- "Wire up the no-as-casting conformance check in CI"
|
|
353
|
-
|
|
354
|
-
If you maintain your own Claude Code skills, consider adding Polly's state-tier decision tree and verification patterns to your project-specific skill so Claude can recommend the right primitive for each piece of state.
|
|
355
|
-
|
|
356
|
-
## Licence
|
|
357
|
-
|
|
358
|
-
MIT
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
[GitHub](https://github.com/AlexJeffcott/polly) · [Issues](https://github.com/AlexJeffcott/polly/issues) · [Examples](https://github.com/AlexJeffcott/polly/tree/main/examples)
|