@fairfox/polly 0.10.0 → 0.11.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/README.md +186 -0
- package/dist/tools/teach/src/cli.js +237 -13
- package/dist/tools/teach/src/cli.js.map +4 -4
- package/dist/tools/teach/src/index.js +89 -13
- package/dist/tools/teach/src/index.js.map +3 -3
- package/dist/tools/verify/src/cli.js +95 -16
- package/dist/tools/verify/src/cli.js.map +5 -5
- package/dist/tools/verify/src/config.d.ts +1 -1
- package/dist/tools/verify/src/config.js +6 -2
- package/dist/tools/verify/src/config.js.map +5 -4
- package/dist/tools/verify/src/primitives/index.d.ts +90 -0
- package/dist/tools/visualize/src/cli.js +89 -13
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +21 -2
package/README.md
CHANGED
|
@@ -343,6 +343,192 @@ test('user profile updates', async ({ page, extensionId }) => {
|
|
|
343
343
|
})
|
|
344
344
|
```
|
|
345
345
|
|
|
346
|
+
### Full-Stack SPAs with Elysia (Bun)
|
|
347
|
+
|
|
348
|
+
Polly provides first-class support for building full-stack web applications with Elysia and Bun, treating your SPA as a distributed system.
|
|
349
|
+
|
|
350
|
+
**Why?** Modern SPAs are distributed systems facing classic distributed computing problems: network unreliability, eventual consistency, offline behavior, cache invalidation, and the CAP theorem. The Elysia integration makes these concerns explicit and verifiable.
|
|
351
|
+
|
|
352
|
+
#### Server: Add Polly Middleware
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// server/index.ts
|
|
356
|
+
import { Elysia, t } from 'elysia'
|
|
357
|
+
import { polly } from '@fairfox/polly/elysia'
|
|
358
|
+
import { $syncedState, $serverState } from '@fairfox/polly'
|
|
359
|
+
|
|
360
|
+
const app = new Elysia()
|
|
361
|
+
.use(polly({
|
|
362
|
+
// Define shared state
|
|
363
|
+
state: {
|
|
364
|
+
client: {
|
|
365
|
+
todos: $syncedState('todos', []),
|
|
366
|
+
user: $syncedState('user', null),
|
|
367
|
+
},
|
|
368
|
+
server: {
|
|
369
|
+
db: $serverState('db', database),
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
// Define client-side effects (what happens after server operations)
|
|
374
|
+
effects: {
|
|
375
|
+
'POST /todos': {
|
|
376
|
+
client: ({ result, state }) => {
|
|
377
|
+
// Update client state with new todo
|
|
378
|
+
state.client.todos.value = [...state.client.todos.value, result]
|
|
379
|
+
},
|
|
380
|
+
broadcast: true, // Notify all connected clients
|
|
381
|
+
},
|
|
382
|
+
'PATCH /todos/:id': {
|
|
383
|
+
client: ({ result, state }) => {
|
|
384
|
+
// Update specific todo in client state
|
|
385
|
+
state.client.todos.value = state.client.todos.value.map(t =>
|
|
386
|
+
t.id === result.id ? result : t
|
|
387
|
+
)
|
|
388
|
+
},
|
|
389
|
+
broadcast: true,
|
|
390
|
+
},
|
|
391
|
+
'DELETE /todos/:id': {
|
|
392
|
+
client: ({ params, state }) => {
|
|
393
|
+
// Remove todo from client state
|
|
394
|
+
state.client.todos.value = state.client.todos.value.filter(
|
|
395
|
+
t => t.id !== Number(params.id)
|
|
396
|
+
)
|
|
397
|
+
},
|
|
398
|
+
broadcast: true,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
// Define authorization rules
|
|
403
|
+
authorization: {
|
|
404
|
+
'POST /todos': ({ state }) => state.client.user.value !== null,
|
|
405
|
+
'PATCH /todos/:id': ({ state }) => state.client.user.value !== null,
|
|
406
|
+
'DELETE /todos/:id': ({ state }) => state.client.user.value !== null,
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
// Configure offline behavior
|
|
410
|
+
offline: {
|
|
411
|
+
'POST /todos': {
|
|
412
|
+
queue: true, // Queue when offline
|
|
413
|
+
optimistic: (body) => ({
|
|
414
|
+
id: -Date.now(), // Temporary ID
|
|
415
|
+
text: body.text,
|
|
416
|
+
completed: false,
|
|
417
|
+
}),
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
// Enable TLA+ generation for verification
|
|
422
|
+
tlaGeneration: true,
|
|
423
|
+
}))
|
|
424
|
+
|
|
425
|
+
// Write normal Elysia routes (no Polly annotations!)
|
|
426
|
+
.post('/todos', async ({ body, pollyState }) => {
|
|
427
|
+
const todo = await pollyState.server.db.value.todos.create(body)
|
|
428
|
+
return todo
|
|
429
|
+
}, {
|
|
430
|
+
body: t.Object({ text: t.String() })
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
.listen(3000)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
#### Client: Use Eden with Polly Wrapper
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// client/api.ts
|
|
440
|
+
import { createPollyClient } from '@fairfox/polly/client'
|
|
441
|
+
import { $syncedState } from '@fairfox/polly'
|
|
442
|
+
import type { app } from '../server' // Import server type!
|
|
443
|
+
|
|
444
|
+
// Define client state
|
|
445
|
+
export const clientState = {
|
|
446
|
+
todos: $syncedState('todos', []),
|
|
447
|
+
user: $syncedState('user', null),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Create type-safe API client (types inferred from server!)
|
|
451
|
+
export const api = createPollyClient<typeof app>('http://localhost:3000', {
|
|
452
|
+
state: clientState,
|
|
453
|
+
websocket: true, // Enable real-time updates
|
|
454
|
+
})
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// client/components/TodoList.tsx
|
|
459
|
+
import { useSignal } from '@preact/signals'
|
|
460
|
+
import { api, clientState } from '../api'
|
|
461
|
+
|
|
462
|
+
export function TodoList() {
|
|
463
|
+
const newTodo = useSignal('')
|
|
464
|
+
|
|
465
|
+
async function handleAdd() {
|
|
466
|
+
// Automatically handles:
|
|
467
|
+
// - Optimistic update if offline
|
|
468
|
+
// - Queue for retry
|
|
469
|
+
// - Execute client effect on success
|
|
470
|
+
// - Broadcast to other clients
|
|
471
|
+
await api.todos.post({ text: newTodo.value })
|
|
472
|
+
newTodo.value = ''
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<div>
|
|
477
|
+
{/* Connection status */}
|
|
478
|
+
<div>Status: {api.$polly.state.isOnline.value ? '🟢 Online' : '🔴 Offline'}</div>
|
|
479
|
+
|
|
480
|
+
{/* Queued requests indicator */}
|
|
481
|
+
{api.$polly.state.queuedRequests.value.length > 0 && (
|
|
482
|
+
<div>{api.$polly.state.queuedRequests.value.length} requests queued</div>
|
|
483
|
+
)}
|
|
484
|
+
|
|
485
|
+
{/* Todo list (automatically updates from state) */}
|
|
486
|
+
<ul>
|
|
487
|
+
{clientState.todos.value.map(todo => (
|
|
488
|
+
<li key={todo.id}>
|
|
489
|
+
<input
|
|
490
|
+
type="checkbox"
|
|
491
|
+
checked={todo.completed}
|
|
492
|
+
onChange={() => api.todos[todo.id].patch({ completed: !todo.completed })}
|
|
493
|
+
/>
|
|
494
|
+
<span>{todo.text}</span>
|
|
495
|
+
<button onClick={() => api.todos[todo.id].delete()}>Delete</button>
|
|
496
|
+
</li>
|
|
497
|
+
))}
|
|
498
|
+
</ul>
|
|
499
|
+
|
|
500
|
+
{/* Add new todo */}
|
|
501
|
+
<input
|
|
502
|
+
value={newTodo.value}
|
|
503
|
+
onInput={(e) => newTodo.value = e.currentTarget.value}
|
|
504
|
+
placeholder="What needs to be done?"
|
|
505
|
+
/>
|
|
506
|
+
<button onClick={handleAdd}>Add</button>
|
|
507
|
+
</div>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
#### Key Benefits
|
|
513
|
+
|
|
514
|
+
1. **Zero Type Duplication** - Eden infers client types from Elysia routes automatically
|
|
515
|
+
2. **Distributed Systems Semantics** - Explicit offline, authorization, and effects configuration
|
|
516
|
+
3. **Production-Ready** - Middleware is pass-through in production (minimal overhead)
|
|
517
|
+
4. **Real-Time Updates** - WebSocket broadcast keeps all clients in sync
|
|
518
|
+
5. **Formal Verification** - Generate TLA+ specs from middleware config to verify distributed properties
|
|
519
|
+
|
|
520
|
+
#### Production vs Development
|
|
521
|
+
|
|
522
|
+
**Development Mode:**
|
|
523
|
+
- Middleware adds metadata to responses for hot-reload and debugging
|
|
524
|
+
- Client effects serialized from server for live updates
|
|
525
|
+
- TLA+ generation enabled for verification
|
|
526
|
+
|
|
527
|
+
**Production Mode:**
|
|
528
|
+
- Middleware is minimal (authorization + broadcast only)
|
|
529
|
+
- Client effects are bundled at build time
|
|
530
|
+
- Zero serialization overhead
|
|
531
|
+
|
|
346
532
|
## Core Concepts
|
|
347
533
|
|
|
348
534
|
### State Primitives
|
|
@@ -5435,40 +5435,116 @@ class HandlerExtractor {
|
|
|
5435
5435
|
project;
|
|
5436
5436
|
typeGuardCache;
|
|
5437
5437
|
relationshipExtractor;
|
|
5438
|
+
analyzedFiles;
|
|
5439
|
+
packageRoot;
|
|
5438
5440
|
constructor(tsConfigPath) {
|
|
5439
5441
|
this.project = new Project3({
|
|
5440
5442
|
tsConfigFilePath: tsConfigPath
|
|
5441
5443
|
});
|
|
5442
5444
|
this.typeGuardCache = new WeakMap;
|
|
5443
5445
|
this.relationshipExtractor = new RelationshipExtractor;
|
|
5446
|
+
this.analyzedFiles = new Set;
|
|
5447
|
+
this.packageRoot = this.findPackageRoot(tsConfigPath);
|
|
5448
|
+
}
|
|
5449
|
+
findPackageRoot(tsConfigPath) {
|
|
5450
|
+
let dir = tsConfigPath.substring(0, tsConfigPath.lastIndexOf("/"));
|
|
5451
|
+
while (dir.length > 1) {
|
|
5452
|
+
try {
|
|
5453
|
+
const packageJsonPath = `${dir}/package.json`;
|
|
5454
|
+
const file = Bun.file(packageJsonPath);
|
|
5455
|
+
if (file.size > 0) {
|
|
5456
|
+
return dir;
|
|
5457
|
+
}
|
|
5458
|
+
} catch {}
|
|
5459
|
+
const parentDir = dir.substring(0, dir.lastIndexOf("/"));
|
|
5460
|
+
if (parentDir === dir)
|
|
5461
|
+
break;
|
|
5462
|
+
dir = parentDir;
|
|
5463
|
+
}
|
|
5464
|
+
return tsConfigPath.substring(0, tsConfigPath.lastIndexOf("/"));
|
|
5444
5465
|
}
|
|
5445
5466
|
extractHandlers() {
|
|
5446
5467
|
const handlers = [];
|
|
5447
5468
|
const messageTypes = new Set;
|
|
5448
5469
|
const invalidMessageTypes = new Set;
|
|
5449
5470
|
const stateConstraints = [];
|
|
5450
|
-
const
|
|
5451
|
-
this.
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
|
|
5456
|
-
const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
|
|
5457
|
-
stateConstraints.push(...fileConstraints);
|
|
5471
|
+
const allSourceFiles = this.project.getSourceFiles();
|
|
5472
|
+
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
5473
|
+
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
5474
|
+
for (const entryPoint of entryPoints) {
|
|
5475
|
+
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints);
|
|
5458
5476
|
}
|
|
5459
5477
|
this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
|
|
5478
|
+
this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
|
|
5460
5479
|
return {
|
|
5461
5480
|
handlers,
|
|
5462
5481
|
messageTypes,
|
|
5463
5482
|
stateConstraints
|
|
5464
5483
|
};
|
|
5465
5484
|
}
|
|
5466
|
-
|
|
5485
|
+
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints) {
|
|
5486
|
+
const filePath = sourceFile.getFilePath();
|
|
5487
|
+
if (this.analyzedFiles.has(filePath)) {
|
|
5488
|
+
return;
|
|
5489
|
+
}
|
|
5490
|
+
this.analyzedFiles.add(filePath);
|
|
5491
|
+
if (process.env["POLLY_DEBUG"]) {
|
|
5492
|
+
console.log(`[DEBUG] Analyzing: ${filePath}`);
|
|
5493
|
+
}
|
|
5494
|
+
const fileHandlers = this.extractFromFile(sourceFile);
|
|
5495
|
+
handlers.push(...fileHandlers);
|
|
5496
|
+
this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
|
|
5497
|
+
const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
|
|
5498
|
+
stateConstraints.push(...fileConstraints);
|
|
5499
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
5500
|
+
for (const importDecl of importDeclarations) {
|
|
5501
|
+
const importedFile = importDecl.getModuleSpecifierSourceFile();
|
|
5502
|
+
if (importedFile) {
|
|
5503
|
+
const importedPath = importedFile.getFilePath();
|
|
5504
|
+
if (!this.isWithinPackage(importedPath)) {
|
|
5505
|
+
if (process.env["POLLY_DEBUG"]) {
|
|
5506
|
+
console.log(`[DEBUG] Skipping external import: ${importedPath}`);
|
|
5507
|
+
}
|
|
5508
|
+
continue;
|
|
5509
|
+
}
|
|
5510
|
+
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints);
|
|
5511
|
+
} else if (process.env["POLLY_DEBUG"]) {
|
|
5512
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
5513
|
+
if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
|
|
5514
|
+
console.log(`[DEBUG] Could not resolve import: ${specifier} in ${filePath}`);
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
isWithinPackage(filePath) {
|
|
5520
|
+
if (!filePath.startsWith(this.packageRoot)) {
|
|
5521
|
+
return false;
|
|
5522
|
+
}
|
|
5523
|
+
if (filePath.includes("/node_modules/")) {
|
|
5524
|
+
return false;
|
|
5525
|
+
}
|
|
5526
|
+
return true;
|
|
5527
|
+
}
|
|
5528
|
+
isNodeModuleImport(specifier) {
|
|
5529
|
+
return !specifier.startsWith(".") && !specifier.startsWith("/");
|
|
5530
|
+
}
|
|
5531
|
+
debugLogAnalysisStats(totalSourceFiles, entryPointCount) {
|
|
5532
|
+
if (!process.env["POLLY_DEBUG"])
|
|
5533
|
+
return;
|
|
5534
|
+
console.log(`[DEBUG] Analysis Statistics:`);
|
|
5535
|
+
console.log(`[DEBUG] Package root: ${this.packageRoot}`);
|
|
5536
|
+
console.log(`[DEBUG] Source files from tsconfig: ${totalSourceFiles}`);
|
|
5537
|
+
console.log(`[DEBUG] Entry points (in package): ${entryPointCount}`);
|
|
5538
|
+
console.log(`[DEBUG] Files analyzed (including imports): ${this.analyzedFiles.size}`);
|
|
5539
|
+
console.log(`[DEBUG] Additional files discovered: ${this.analyzedFiles.size - entryPointCount}`);
|
|
5540
|
+
}
|
|
5541
|
+
debugLogSourceFiles(allSourceFiles, entryPoints) {
|
|
5467
5542
|
if (!process.env["POLLY_DEBUG"])
|
|
5468
5543
|
return;
|
|
5469
|
-
console.log(`[DEBUG] Loaded ${
|
|
5470
|
-
|
|
5471
|
-
|
|
5544
|
+
console.log(`[DEBUG] Loaded ${allSourceFiles.length} source files from tsconfig`);
|
|
5545
|
+
console.log(`[DEBUG] Filtered to ${entryPoints.length} entry points within package`);
|
|
5546
|
+
if (entryPoints.length <= 20) {
|
|
5547
|
+
for (const sf of entryPoints) {
|
|
5472
5548
|
console.log(`[DEBUG] - ${sf.getFilePath()}`);
|
|
5473
5549
|
}
|
|
5474
5550
|
}
|
|
@@ -8810,6 +8886,7 @@ ${generateVerificationSection(context)}
|
|
|
8810
8886
|
- **Performance**: How to optimize verification speed and state space exploration
|
|
8811
8887
|
- **Debugging**: Interpreting counterexamples and fixing violations
|
|
8812
8888
|
- **Configuration**: Understanding maxInFlight, bounds, and other verification parameters
|
|
8889
|
+
- **Elysia Integration**: Using Polly with Elysia/Bun servers for full-stack distributed systems verification
|
|
8813
8890
|
|
|
8814
8891
|
# Important Notes
|
|
8815
8892
|
|
|
@@ -8822,6 +8899,149 @@ ${generateVerificationSection(context)}
|
|
|
8822
8899
|
- Example: \`$constraints("loggedIn", { USER_LOGOUT: { requires: "state.loggedIn === true" } })\`
|
|
8823
8900
|
- This eliminates duplication and creates a single source of truth for state invariants
|
|
8824
8901
|
- Parser extracts constraints and adds them to all relevant message handlers automatically
|
|
8902
|
+
- **File Organization**: Constraints can be organized in separate files (e.g., specs/constraints.ts)
|
|
8903
|
+
- **Transitive Discovery**: The analyzer uses transitive import following to discover constraints
|
|
8904
|
+
- Files outside src/ are automatically found if imported from handler files
|
|
8905
|
+
- This enables clean separation of verification code from runtime code
|
|
8906
|
+
|
|
8907
|
+
# Elysia/Bun Integration
|
|
8908
|
+
|
|
8909
|
+
Polly now provides first-class support for Elysia (Bun-first web framework) with Eden type-safe client generation:
|
|
8910
|
+
|
|
8911
|
+
## Server-Side Middleware (\`@fairfox/polly/elysia\`)
|
|
8912
|
+
|
|
8913
|
+
The \`polly()\` middleware adds distributed systems semantics to Elysia apps:
|
|
8914
|
+
|
|
8915
|
+
**Key Features:**
|
|
8916
|
+
- **State Management**: Define client and server state signals
|
|
8917
|
+
- **Client Effects**: Specify what should happen on the client after server operations
|
|
8918
|
+
- **Authorization**: Route-level authorization rules
|
|
8919
|
+
- **Offline Behavior**: Configure optimistic updates and queueing
|
|
8920
|
+
- **WebSocket Broadcast**: Real-time updates to connected clients
|
|
8921
|
+
- **TLA+ Generation**: Automatic formal specification generation from routes + config
|
|
8922
|
+
|
|
8923
|
+
**Example:**
|
|
8924
|
+
\`\`\`typescript
|
|
8925
|
+
import { Elysia, t } from 'elysia';
|
|
8926
|
+
import { polly } from '@fairfox/polly/elysia';
|
|
8927
|
+
import { $syncedState, $serverState } from '@fairfox/polly';
|
|
8928
|
+
|
|
8929
|
+
const app = new Elysia()
|
|
8930
|
+
.use(polly({
|
|
8931
|
+
state: {
|
|
8932
|
+
client: {
|
|
8933
|
+
todos: $syncedState('todos', []),
|
|
8934
|
+
user: $syncedState('user', null),
|
|
8935
|
+
},
|
|
8936
|
+
server: {
|
|
8937
|
+
db: $serverState('db', db),
|
|
8938
|
+
},
|
|
8939
|
+
},
|
|
8940
|
+
effects: {
|
|
8941
|
+
'POST /todos': {
|
|
8942
|
+
client: ({ result, state }) => {
|
|
8943
|
+
state.client.todos.value = [...state.client.todos.value, result];
|
|
8944
|
+
},
|
|
8945
|
+
broadcast: true, // Send to all connected clients
|
|
8946
|
+
},
|
|
8947
|
+
},
|
|
8948
|
+
authorization: {
|
|
8949
|
+
'POST /todos': ({ state }) => state.client.user.value !== null,
|
|
8950
|
+
},
|
|
8951
|
+
offline: {
|
|
8952
|
+
'POST /todos': {
|
|
8953
|
+
queue: true,
|
|
8954
|
+
optimistic: (body) => ({ id: -Date.now(), ...body }),
|
|
8955
|
+
},
|
|
8956
|
+
},
|
|
8957
|
+
}))
|
|
8958
|
+
.post('/todos', handler, { body: t.Object({ text: t.String() }) });
|
|
8959
|
+
\`\`\`
|
|
8960
|
+
|
|
8961
|
+
**Route Pattern Matching:**
|
|
8962
|
+
- Exact: \`'POST /todos'\`
|
|
8963
|
+
- Params: \`'GET /todos/:id'\`
|
|
8964
|
+
- Wildcard: \`'/todos/*'\`
|
|
8965
|
+
|
|
8966
|
+
**Production Behavior:**
|
|
8967
|
+
- In development: Adds metadata to responses for hot-reload and debugging
|
|
8968
|
+
- In production: Pass-through (minimal overhead) - client effects are bundled at build time
|
|
8969
|
+
- Authorization and broadcasts work in both modes
|
|
8970
|
+
|
|
8971
|
+
## Client-Side Wrapper (\`@fairfox/polly/client\`)
|
|
8972
|
+
|
|
8973
|
+
Enhances Eden treaty client with Polly features:
|
|
8974
|
+
|
|
8975
|
+
**Example:**
|
|
8976
|
+
\`\`\`typescript
|
|
8977
|
+
import { createPollyClient } from '@fairfox/polly/client';
|
|
8978
|
+
import { $syncedState } from '@fairfox/polly';
|
|
8979
|
+
import type { app } from './server';
|
|
8980
|
+
|
|
8981
|
+
const clientState = {
|
|
8982
|
+
todos: $syncedState('todos', []),
|
|
8983
|
+
user: $syncedState('user', null),
|
|
8984
|
+
};
|
|
8985
|
+
|
|
8986
|
+
export const api = createPollyClient<typeof app>('http://localhost:3000', {
|
|
8987
|
+
state: clientState,
|
|
8988
|
+
websocket: true, // Enable real-time updates
|
|
8989
|
+
});
|
|
8990
|
+
|
|
8991
|
+
// Use it (types are automatically inferred from server!)
|
|
8992
|
+
await api.todos.post({ text: 'Buy milk' });
|
|
8993
|
+
|
|
8994
|
+
// Access Polly features
|
|
8995
|
+
console.log(api.$polly.state.isOnline.value); // true/false
|
|
8996
|
+
console.log(api.$polly.state.queuedRequests.value); // Queued requests
|
|
8997
|
+
api.$polly.sync(); // Manually sync queued requests
|
|
8998
|
+
\`\`\`
|
|
8999
|
+
|
|
9000
|
+
**Key Features:**
|
|
9001
|
+
- Offline queueing with automatic retry
|
|
9002
|
+
- WebSocket connection for real-time updates
|
|
9003
|
+
- Lamport clock synchronization
|
|
9004
|
+
- Type inference from server via Eden
|
|
9005
|
+
|
|
9006
|
+
## Why This Matters for Distributed Systems
|
|
9007
|
+
|
|
9008
|
+
SPAs/PWAs are distributed systems facing classic problems:
|
|
9009
|
+
- **CAP theorem**: Must choose consistency vs availability during partitions
|
|
9010
|
+
- **Network unreliability**: The first fallacy of distributed computing
|
|
9011
|
+
- **Cache invalidation**: "One of the two hard things in computer science"
|
|
9012
|
+
- **Eventual consistency**: State sync across client/server
|
|
9013
|
+
- **Conflict resolution**: When multiple devices edit offline
|
|
9014
|
+
|
|
9015
|
+
The Elysia integration addresses this by:
|
|
9016
|
+
1. Making distributed concerns explicit (offline, authorization, effects)
|
|
9017
|
+
2. Leveraging Eden for zero-duplication type safety
|
|
9018
|
+
3. Supporting verification via TLA+ generation from middleware config
|
|
9019
|
+
4. Providing WebSocket broadcast for real-time consistency
|
|
9020
|
+
|
|
9021
|
+
## Architecture Pattern
|
|
9022
|
+
|
|
9023
|
+
\`\`\`
|
|
9024
|
+
Browser (Client)
|
|
9025
|
+
├── Eden Treaty Client (types from Elysia)
|
|
9026
|
+
├── Polly Wrapper (offline, sync, WebSocket)
|
|
9027
|
+
└── Client State ($syncedState)
|
|
9028
|
+
│
|
|
9029
|
+
│ HTTP / WebSocket
|
|
9030
|
+
│
|
|
9031
|
+
Server (Elysia + Bun)
|
|
9032
|
+
├── Elysia Routes (normal routes)
|
|
9033
|
+
├── Polly Middleware (effects, auth, broadcast)
|
|
9034
|
+
└── Server State ($serverState)
|
|
9035
|
+
\`\`\`
|
|
9036
|
+
|
|
9037
|
+
## Best Practices
|
|
9038
|
+
|
|
9039
|
+
1. **Separate Elysia is the contract** - Don't define types twice. Elysia routes define the API, Eden generates client types.
|
|
9040
|
+
2. **Effects describe client behavior** - Keep effects pure and deterministic.
|
|
9041
|
+
3. **Authorization at route level** - Centralize security rules in middleware config.
|
|
9042
|
+
4. **Queue selectively** - Only queue idempotent operations when offline.
|
|
9043
|
+
5. **Broadcast with filters** - Use broadcastFilter to target specific clients.
|
|
9044
|
+
6. **Generate TLA+ for verification** - Enable tlaGeneration in dev to verify distributed properties.
|
|
8825
9045
|
|
|
8826
9046
|
Begin by understanding their question and providing a clear, precise answer based on their project context.`;
|
|
8827
9047
|
}
|
|
@@ -8968,6 +9188,10 @@ to reduce verification time while maintaining or improving verification precisio
|
|
|
8968
9188
|
and available in the current version of Polly. You can recommend any of these optimizations with
|
|
8969
9189
|
confidence that they will work when users apply them to their configuration.
|
|
8970
9190
|
|
|
9191
|
+
**Code Organization**: The analyzer uses transitive import following to discover all reachable code.
|
|
9192
|
+
Constraints and type guards can be organized in separate files (e.g., specs/constraints.ts) and will
|
|
9193
|
+
be automatically discovered via imports. Files outside src/ are fully supported.
|
|
9194
|
+
|
|
8971
9195
|
# Communication Style
|
|
8972
9196
|
|
|
8973
9197
|
- Direct and precise - no fluff
|
|
@@ -9284,4 +9508,4 @@ Goodbye!`);
|
|
|
9284
9508
|
}
|
|
9285
9509
|
main();
|
|
9286
9510
|
|
|
9287
|
-
//# debugId=
|
|
9511
|
+
//# debugId=CDFF2E3A99C1DA5C64756E2164756E21
|