@fairfox/polly 0.11.0 → 0.12.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.
Files changed (40) hide show
  1. package/dist/src/client/index.d.ts +33 -0
  2. package/dist/src/client/index.js +586 -0
  3. package/dist/src/client/index.js.map +13 -0
  4. package/dist/src/client/wrapper.d.ts +54 -0
  5. package/dist/src/core/clock.d.ts +63 -0
  6. package/dist/src/elysia/index.d.ts +43 -0
  7. package/dist/src/elysia/index.js +241 -0
  8. package/dist/src/elysia/index.js.map +12 -0
  9. package/dist/src/elysia/plugin.d.ts +5 -0
  10. package/dist/src/elysia/tla-generator.d.ts +16 -0
  11. package/dist/src/elysia/types.d.ts +137 -0
  12. package/dist/src/utils/function-serialization.d.ts +14 -0
  13. package/dist/tools/analysis/src/extract/adr.d.ts +37 -0
  14. package/dist/tools/analysis/src/extract/architecture.d.ts +42 -0
  15. package/dist/tools/analysis/src/extract/contexts.d.ts +74 -0
  16. package/dist/tools/analysis/src/extract/flows.d.ts +68 -0
  17. package/dist/tools/analysis/src/extract/handlers.d.ts +330 -0
  18. package/dist/tools/analysis/src/extract/index.d.ts +9 -0
  19. package/dist/tools/analysis/src/extract/integrations.d.ts +77 -0
  20. package/dist/tools/analysis/src/extract/manifest.d.ts +64 -0
  21. package/dist/tools/analysis/src/extract/project-detector.d.ts +103 -0
  22. package/dist/tools/analysis/src/extract/relationships.d.ts +119 -0
  23. package/dist/tools/analysis/src/extract/types.d.ts +139 -0
  24. package/dist/tools/analysis/src/index.d.ts +2 -0
  25. package/dist/tools/analysis/src/types/adr.d.ts +39 -0
  26. package/dist/tools/analysis/src/types/architecture.d.ts +198 -0
  27. package/dist/tools/analysis/src/types/core.d.ts +178 -0
  28. package/dist/tools/analysis/src/types/index.d.ts +4 -0
  29. package/dist/tools/teach/src/cli.js +140 -69
  30. package/dist/tools/teach/src/cli.js.map +12 -12
  31. package/dist/tools/teach/src/index.d.ts +28 -0
  32. package/dist/tools/teach/src/index.js +145 -72
  33. package/dist/tools/teach/src/index.js.map +13 -13
  34. package/dist/tools/verify/src/cli.js +33 -11
  35. package/dist/tools/verify/src/cli.js.map +5 -5
  36. package/dist/tools/visualize/src/cli.js +125 -66
  37. package/dist/tools/visualize/src/cli.js.map +11 -11
  38. package/dist/tools/visualize/src/codegen/structurizr.d.ts +343 -0
  39. package/dist/tools/visualize/src/types/structurizr.d.ts +235 -0
  40. package/package.json +6 -5
@@ -0,0 +1,54 @@
1
+ import { type Signal } from "@preact/signals-core";
2
+ /**
3
+ * Polly client options
4
+ */
5
+ export interface PollyClientOptions {
6
+ /**
7
+ * Client state signals that should be synced
8
+ */
9
+ state?: Record<string, Signal<unknown>>;
10
+ /**
11
+ * Callback when online/offline status changes
12
+ */
13
+ onOfflineChange?: (isOnline: boolean) => void;
14
+ /**
15
+ * Enable WebSocket for real-time updates (default: true in dev, false in prod)
16
+ */
17
+ websocket?: boolean;
18
+ /**
19
+ * WebSocket path (default: '/polly/ws')
20
+ */
21
+ websocketPath?: string;
22
+ }
23
+ /**
24
+ * Create a Polly-enhanced Eden client
25
+ *
26
+ * In DEV mode:
27
+ * - Processes server metadata for hot reloading
28
+ * - Executes client effects from server
29
+ * - Handles offline queueing
30
+ * - Connects WebSocket for real-time updates
31
+ *
32
+ * In PROD mode:
33
+ * - Minimal wrapper (client effects are bundled)
34
+ * - Optional WebSocket for real-time features
35
+ * - Offline queueing still works
36
+ *
37
+ * Example:
38
+ * ```typescript
39
+ * import { createPollyClient } from '@fairfox/polly/client';
40
+ * import { $syncedState } from '@fairfox/polly';
41
+ * import type { app } from './server';
42
+ *
43
+ * const clientState = {
44
+ * todos: $syncedState('todos', []),
45
+ * user: $syncedState('user', null),
46
+ * };
47
+ *
48
+ * export const api = createPollyClient<typeof app>('http://localhost:3000', {
49
+ * state: clientState,
50
+ * websocket: true,
51
+ * });
52
+ * ```
53
+ */
54
+ export declare function createPollyClient<T extends Record<string, unknown>>(url: string, options?: PollyClientOptions): any;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Lamport Clock Implementation
3
+ *
4
+ * Provides logical timestamps for distributed systems to establish
5
+ * causal ordering of events across different contexts (client/server).
6
+ *
7
+ * Key properties:
8
+ * - Each event increments the local clock
9
+ * - When receiving a message, clock = max(local, received) + 1
10
+ * - If A happens before B, then timestamp(A) < timestamp(B)
11
+ *
12
+ * References:
13
+ * - Lamport, L. (1978). "Time, Clocks, and the Ordering of Events in a Distributed System"
14
+ * - https://lamport.azurewebsites.net/pubs/time-clocks.pdf
15
+ */
16
+ /**
17
+ * Lamport clock state
18
+ */
19
+ export interface LamportClock {
20
+ tick: number;
21
+ contextId: string;
22
+ }
23
+ /**
24
+ * Lamport clock with operations
25
+ */
26
+ export interface LamportClockOps {
27
+ /**
28
+ * Get current clock value
29
+ */
30
+ now(): LamportClock;
31
+ /**
32
+ * Increment the clock (before sending a message or performing an action)
33
+ */
34
+ tick(): number;
35
+ /**
36
+ * Update clock when receiving a message
37
+ * Sets clock to max(local, received) + 1
38
+ */
39
+ update(receivedClock: LamportClock): void;
40
+ }
41
+ /**
42
+ * Create a Lamport clock for a specific context
43
+ *
44
+ * @param contextId - Unique identifier for this context (e.g., "client", "server", "worker-1")
45
+ * @returns Clock operations
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const serverClock = createLamportClock("server");
50
+ *
51
+ * // Before sending a message
52
+ * serverClock.tick();
53
+ * const timestamp = serverClock.now();
54
+ * send({ data: "...", clock: timestamp });
55
+ *
56
+ * // When receiving a message
57
+ * onReceive((message) => {
58
+ * serverClock.update(message.clock);
59
+ * // Process message with updated clock
60
+ * });
61
+ * ```
62
+ */
63
+ export declare function createLamportClock(contextId: string): LamportClockOps;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Polly Elysia Middleware
3
+ *
4
+ * Adds distributed systems semantics to Elysia apps:
5
+ * - State management (client + server)
6
+ * - Authorization
7
+ * - Offline behavior
8
+ * - WebSocket broadcast for real-time updates
9
+ * - TLA+ generation for verification
10
+ *
11
+ * Example:
12
+ * ```typescript
13
+ * import { Elysia } from 'elysia';
14
+ * import { polly } from '@fairfox/polly/elysia';
15
+ * import { $syncedState, $serverState } from '@fairfox/polly';
16
+ *
17
+ * const app = new Elysia()
18
+ * .use(polly({
19
+ * state: {
20
+ * client: {
21
+ * todos: $syncedState('todos', []),
22
+ * },
23
+ * server: {
24
+ * db: $serverState('db', db),
25
+ * },
26
+ * },
27
+ * effects: {
28
+ * 'POST /todos': {
29
+ * client: ({ result, state }) => {
30
+ * state.client.todos.value = [...state.client.todos.value, result];
31
+ * },
32
+ * broadcast: true,
33
+ * },
34
+ * },
35
+ * authorization: {
36
+ * 'POST /todos': ({ state }) => state.client.user.value !== null,
37
+ * },
38
+ * }))
39
+ * .post('/todos', handler, { body: t.Object({ text: t.String() }) });
40
+ * ```
41
+ */
42
+ export { polly } from "./plugin";
43
+ export type { AuthorizationContext, AuthorizationHandler, ClientEffectConfig, EffectContext, OfflineConfig, PollyConfig, PollyResponseMetadata, PollyStateConfig, RoutePattern, } from "./types";
@@ -0,0 +1,241 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
19
+ var __toCommonJS = (from) => {
20
+ var entry = __moduleCache.get(from), desc;
21
+ if (entry)
22
+ return entry;
23
+ entry = __defProp({}, "__esModule", { value: true });
24
+ if (from && typeof from === "object" || typeof from === "function")
25
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
26
+ get: () => from[key],
27
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
28
+ }));
29
+ __moduleCache.set(from, entry);
30
+ return entry;
31
+ };
32
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __export = (target, all) => {
34
+ for (var name in all)
35
+ __defProp(target, name, {
36
+ get: all[name],
37
+ enumerable: true,
38
+ configurable: true,
39
+ set: (newValue) => all[name] = () => newValue
40
+ });
41
+ };
42
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
43
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
44
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
45
+ }) : x)(function(x) {
46
+ if (typeof require !== "undefined")
47
+ return require.apply(this, arguments);
48
+ throw Error('Dynamic require of "' + x + '" is not supported');
49
+ });
50
+
51
+ // src/elysia/plugin.ts
52
+ import { Elysia } from "elysia";
53
+
54
+ // src/core/clock.ts
55
+ function createLamportClock(contextId) {
56
+ let tick = 0;
57
+ return {
58
+ now() {
59
+ return { tick, contextId };
60
+ },
61
+ tick() {
62
+ tick += 1;
63
+ return tick;
64
+ },
65
+ update(receivedClock) {
66
+ tick = Math.max(tick, receivedClock.tick) + 1;
67
+ }
68
+ };
69
+ }
70
+
71
+ // src/utils/function-serialization.ts
72
+ import serialize from "serialize-javascript";
73
+ var isDev = true;
74
+ function serializeFunction(fn) {
75
+ if (!isDev) {
76
+ return "";
77
+ }
78
+ return serialize(fn, { space: 0 });
79
+ }
80
+ function deserializeFunction(serialized) {
81
+ if (!isDev) {
82
+ throw new Error("[Polly] deserializeFunction should not be called in production. " + "Client effects should be imported from your bundle.");
83
+ }
84
+ if (!serialized) {
85
+ throw new Error("[Polly] Cannot deserialize empty function");
86
+ }
87
+ return eval(`(${serialized})`);
88
+ }
89
+
90
+ // src/elysia/plugin.ts
91
+ function matchRoute(pattern, method, path) {
92
+ const hasMethod = pattern.includes(" ");
93
+ const patternMethod = hasMethod ? pattern.split(" ")[0] : null;
94
+ const patternPath = hasMethod ? pattern.split(" ")[1] : pattern;
95
+ if (patternMethod && patternMethod !== method) {
96
+ return false;
97
+ }
98
+ const patternSegments = patternPath.split("/").filter(Boolean);
99
+ const pathSegments = path.split("/").filter(Boolean);
100
+ if (patternSegments.length !== pathSegments.length && !patternPath.includes("*")) {
101
+ return false;
102
+ }
103
+ for (let i = 0;i < patternSegments.length; i++) {
104
+ const patternSeg = patternSegments[i];
105
+ const pathSeg = pathSegments[i];
106
+ if (patternSeg === "*")
107
+ return true;
108
+ if (patternSeg.startsWith(":"))
109
+ continue;
110
+ if (patternSeg !== pathSeg)
111
+ return false;
112
+ }
113
+ return true;
114
+ }
115
+ function findMatchingConfig(configs, method, path) {
116
+ if (!configs)
117
+ return;
118
+ for (const [pattern, config] of Object.entries(configs)) {
119
+ if (matchRoute(pattern, method, path)) {
120
+ return config;
121
+ }
122
+ }
123
+ return;
124
+ }
125
+
126
+ class BroadcastManager {
127
+ connections = new Map;
128
+ register(clientId, ws) {
129
+ this.connections.set(clientId, ws);
130
+ }
131
+ unregister(clientId) {
132
+ this.connections.delete(clientId);
133
+ }
134
+ broadcast(message, filter) {
135
+ const payload = JSON.stringify(message);
136
+ for (const [clientId, ws] of this.connections.entries()) {
137
+ if (filter && !filter(clientId))
138
+ continue;
139
+ if (ws.readyState === 1) {
140
+ ws.send(payload);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ function polly(config = {}) {
146
+ const isDev2 = true;
147
+ const clock = createLamportClock("server");
148
+ const broadcaster = new BroadcastManager;
149
+ const clientStateByConnection = new Map;
150
+ const app = new Elysia({ name: "polly" }).decorate("pollyState", {
151
+ client: config.state?.client || {},
152
+ server: config.state?.server || {}
153
+ }).decorate("pollyClock", clock).decorate("pollyBroadcast", broadcaster).ws(config.websocketPath || "/polly/ws", {
154
+ open(ws) {
155
+ const clientId = ws.data.headers?.["x-client-id"] || crypto.randomUUID();
156
+ broadcaster.register(clientId, ws.raw);
157
+ ws.send(JSON.stringify({
158
+ type: "state-sync",
159
+ state: config.state?.client || {},
160
+ clock: clock.now()
161
+ }));
162
+ },
163
+ close(ws) {
164
+ const clientId = ws.data.headers?.["x-client-id"];
165
+ if (clientId) {
166
+ broadcaster.unregister(clientId);
167
+ clientStateByConnection.delete(clientId);
168
+ }
169
+ },
170
+ message(ws, message) {
171
+ const data = JSON.parse(message);
172
+ if (data.type === "state-update") {
173
+ const clientId = ws.data.headers?.["x-client-id"];
174
+ if (clientId) {
175
+ clientStateByConnection.set(clientId, data.state);
176
+ }
177
+ }
178
+ }
179
+ }).onBeforeHandle(async ({ request, pollyState, body, params }) => {
180
+ const method = request.method;
181
+ const path = new URL(request.url).pathname;
182
+ const authHandler = findMatchingConfig(config.authorization, method, path);
183
+ if (authHandler) {
184
+ const allowed = await authHandler({
185
+ state: pollyState,
186
+ body,
187
+ params,
188
+ headers: Object.fromEntries(request.headers.entries())
189
+ });
190
+ if (!allowed) {
191
+ return new Response("Unauthorized", { status: 403 });
192
+ }
193
+ }
194
+ }).onAfterHandle(async ({ request, response, _pollyState, pollyClock, pollyBroadcast, _body, _params }) => {
195
+ const method = request.method;
196
+ const path = new URL(request.url).pathname;
197
+ const effectConfig = findMatchingConfig(config.effects, method, path);
198
+ pollyClock.tick();
199
+ if (effectConfig?.broadcast) {
200
+ const broadcastMessage = {
201
+ type: "effect",
202
+ path,
203
+ method,
204
+ result: response,
205
+ clock: pollyClock.now()
206
+ };
207
+ if (effectConfig.broadcastFilter) {
208
+ pollyBroadcast.broadcast(broadcastMessage, (clientId) => {
209
+ const clientState = clientStateByConnection.get(clientId) || {};
210
+ return effectConfig.broadcastFilter?.(clientState) ?? false;
211
+ });
212
+ } else {
213
+ pollyBroadcast.broadcast(broadcastMessage);
214
+ }
215
+ }
216
+ if (!isDev2) {
217
+ return response;
218
+ }
219
+ const offlineConfig = findMatchingConfig(config.offline, method, path);
220
+ const metadata = {
221
+ clientEffect: effectConfig ? {
222
+ handler: serializeFunction(effectConfig.client),
223
+ broadcast: effectConfig.broadcast || false
224
+ } : undefined,
225
+ offline: offlineConfig,
226
+ clock: pollyClock.now()
227
+ };
228
+ return new Response(JSON.stringify(response), {
229
+ headers: {
230
+ "Content-Type": "application/json",
231
+ "X-Polly-Metadata": JSON.stringify(metadata)
232
+ }
233
+ });
234
+ });
235
+ return app;
236
+ }
237
+ export {
238
+ polly
239
+ };
240
+
241
+ //# debugId=8AEBFE7CEBB3C9C364756E2164756E21
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/elysia/plugin.ts", "../src/core/clock.ts", "../src/utils/function-serialization.ts"],
4
+ "sourcesContent": [
5
+ "// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\nimport type { Signal } from \"@preact/signals-core\";\nimport { Elysia } from \"elysia\";\nimport { createLamportClock } from \"../core/clock\";\nimport { serializeFunction } from \"../utils/function-serialization\";\nimport type { PollyConfig, PollyResponseMetadata } from \"./types\";\n\n/**\n * Broadcast message sent to connected clients\n */\ninterface BroadcastMessage {\n type: \"effect\";\n path: string;\n method: string;\n result: unknown;\n clock: { tick: number; contextId: string };\n}\n\n/**\n * Minimal WebSocket interface for broadcasting\n */\ninterface MinimalWebSocket {\n readyState: number;\n send(data: string): void;\n}\n\n/**\n * Route pattern matcher\n * Supports:\n * - Exact match: 'POST /todos'\n * - Param match: 'GET /todos/:id'\n * - Wildcard: '/todos/*'\n */\nfunction matchRoute(pattern: string, method: string, path: string): boolean {\n // Split pattern into method + path or just path\n const hasMethod = pattern.includes(\" \");\n const patternMethod = hasMethod ? pattern.split(\" \")[0] : null;\n const patternPath = hasMethod ? pattern.split(\" \")[1] : pattern;\n\n // Check method\n if (patternMethod && patternMethod !== method) {\n return false;\n }\n\n // Check path\n const patternSegments = patternPath.split(\"/\").filter(Boolean);\n const pathSegments = path.split(\"/\").filter(Boolean);\n\n if (patternSegments.length !== pathSegments.length && !patternPath.includes(\"*\")) {\n return false;\n }\n\n for (let i = 0; i < patternSegments.length; i++) {\n const patternSeg = patternSegments[i];\n const pathSeg = pathSegments[i];\n\n if (patternSeg === \"*\") return true;\n if (patternSeg.startsWith(\":\")) continue; // Param match\n if (patternSeg !== pathSeg) return false;\n }\n\n return true;\n}\n\n/**\n * Find matching config for a route\n */\nfunction findMatchingConfig<T>(\n configs: Record<string, T> | undefined,\n method: string,\n path: string\n): T | undefined {\n if (!configs) return undefined;\n\n for (const [pattern, config] of Object.entries(configs)) {\n if (matchRoute(pattern, method, path)) {\n return config;\n }\n }\n\n return undefined;\n}\n\n/**\n * WebSocket broadcast manager\n */\nclass BroadcastManager {\n private connections = new Map<string, MinimalWebSocket>();\n\n register(clientId: string, ws: MinimalWebSocket) {\n this.connections.set(clientId, ws);\n }\n\n unregister(clientId: string) {\n this.connections.delete(clientId);\n }\n\n broadcast(message: BroadcastMessage, filter?: (clientId: string) => boolean) {\n const payload = JSON.stringify(message);\n\n for (const [clientId, ws] of this.connections.entries()) {\n if (filter && !filter(clientId)) continue;\n if (ws.readyState === 1) {\n // WebSocket.OPEN = 1\n ws.send(payload);\n }\n }\n }\n}\n\n/**\n * Main Polly Elysia plugin\n */\nexport function polly(config: PollyConfig = {}) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const clock = createLamportClock(\"server\");\n const broadcaster = new BroadcastManager();\n const clientStateByConnection = new Map<string, Record<string, Signal<unknown>>>();\n\n const app = new Elysia({ name: \"polly\" })\n // Add state to context\n .decorate(\"pollyState\", {\n client: config.state?.client || {},\n server: config.state?.server || {},\n })\n .decorate(\"pollyClock\", clock)\n .decorate(\"pollyBroadcast\", broadcaster)\n\n // WebSocket endpoint for real-time updates\n .ws(config.websocketPath || \"/polly/ws\", {\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n open(ws) {\n const clientId = ws.data.headers?.[\"x-client-id\"] || crypto.randomUUID();\n broadcaster.register(clientId, ws.raw);\n\n // Send initial state sync\n ws.send(\n JSON.stringify({\n type: \"state-sync\",\n state: config.state?.client || {},\n clock: clock.now(),\n })\n );\n },\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n close(ws) {\n const clientId = ws.data.headers?.[\"x-client-id\"];\n if (clientId) {\n broadcaster.unregister(clientId);\n clientStateByConnection.delete(clientId);\n }\n },\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n message(ws, message) {\n // Handle client state updates\n const data = JSON.parse(message as string);\n\n if (data.type === \"state-update\") {\n const clientId = ws.data.headers?.[\"x-client-id\"];\n if (clientId) {\n clientStateByConnection.set(clientId, data.state);\n }\n }\n },\n })\n\n // Authorization hook (runs before handler)\n // @ts-expect-error - Elysia context types from optional peer dependency\n .onBeforeHandle(async ({ request, pollyState, body, params }) => {\n const method = request.method;\n const path = new URL(request.url).pathname;\n\n const authHandler = findMatchingConfig(config.authorization, method, path);\n\n if (authHandler) {\n const allowed = await authHandler({\n state: pollyState,\n body,\n params,\n headers: Object.fromEntries(request.headers.entries()),\n });\n\n if (!allowed) {\n return new Response(\"Unauthorized\", { status: 403 });\n }\n }\n })\n\n // Response hook (runs after handler)\n // @ts-expect-error - Elysia context types from optional peer dependency\n .onAfterHandle(\n async ({ request, response, _pollyState, pollyClock, pollyBroadcast, _body, _params }) => {\n const method = request.method;\n const path = new URL(request.url).pathname;\n\n // Find matching effect config\n const effectConfig = findMatchingConfig(config.effects, method, path);\n\n // Tick clock\n pollyClock.tick();\n\n // If broadcast enabled, send to all connected clients\n // This works in both dev and prod for real-time updates\n if (effectConfig?.broadcast) {\n const broadcastMessage = {\n type: \"effect\",\n path,\n method,\n result: response,\n clock: pollyClock.now(),\n };\n\n if (effectConfig.broadcastFilter) {\n pollyBroadcast.broadcast(broadcastMessage, (clientId) => {\n const clientState = clientStateByConnection.get(clientId) || {};\n return effectConfig.broadcastFilter?.(clientState) ?? false;\n });\n } else {\n pollyBroadcast.broadcast(broadcastMessage);\n }\n }\n\n // In production, skip expensive metadata operations\n if (!isDev) {\n return response;\n }\n\n // DEV ONLY: Add Polly metadata to response for debugging/hot-reload\n const offlineConfig = findMatchingConfig(config.offline, method, path);\n const metadata: PollyResponseMetadata = {\n clientEffect: effectConfig\n ? {\n handler: serializeFunction(effectConfig.client),\n broadcast: effectConfig.broadcast || false,\n }\n : undefined,\n offline: offlineConfig,\n clock: pollyClock.now(),\n };\n\n // Attach metadata as header\n return new Response(JSON.stringify(response), {\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Polly-Metadata\": JSON.stringify(metadata),\n },\n });\n }\n );\n\n return app;\n}\n",
6
+ "/**\n * Lamport Clock Implementation\n *\n * Provides logical timestamps for distributed systems to establish\n * causal ordering of events across different contexts (client/server).\n *\n * Key properties:\n * - Each event increments the local clock\n * - When receiving a message, clock = max(local, received) + 1\n * - If A happens before B, then timestamp(A) < timestamp(B)\n *\n * References:\n * - Lamport, L. (1978). \"Time, Clocks, and the Ordering of Events in a Distributed System\"\n * - https://lamport.azurewebsites.net/pubs/time-clocks.pdf\n */\n\n/**\n * Lamport clock state\n */\nexport interface LamportClock {\n tick: number;\n contextId: string;\n}\n\n/**\n * Lamport clock with operations\n */\nexport interface LamportClockOps {\n /**\n * Get current clock value\n */\n now(): LamportClock;\n\n /**\n * Increment the clock (before sending a message or performing an action)\n */\n tick(): number;\n\n /**\n * Update clock when receiving a message\n * Sets clock to max(local, received) + 1\n */\n update(receivedClock: LamportClock): void;\n}\n\n/**\n * Create a Lamport clock for a specific context\n *\n * @param contextId - Unique identifier for this context (e.g., \"client\", \"server\", \"worker-1\")\n * @returns Clock operations\n *\n * @example\n * ```typescript\n * const serverClock = createLamportClock(\"server\");\n *\n * // Before sending a message\n * serverClock.tick();\n * const timestamp = serverClock.now();\n * send({ data: \"...\", clock: timestamp });\n *\n * // When receiving a message\n * onReceive((message) => {\n * serverClock.update(message.clock);\n * // Process message with updated clock\n * });\n * ```\n */\nexport function createLamportClock(contextId: string): LamportClockOps {\n let tick = 0;\n\n return {\n now(): LamportClock {\n return { tick, contextId };\n },\n\n tick(): number {\n tick += 1;\n return tick;\n },\n\n update(receivedClock: LamportClock): void {\n tick = Math.max(tick, receivedClock.tick) + 1;\n },\n };\n}\n",
7
+ "import serialize from \"serialize-javascript\";\n\n/**\n * Check if we're in development mode\n */\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * Serialize a function to send to client\n *\n * DEV ONLY: Used for hot reloading and debugging\n * PROD: No-op - client effects are baked into bundle at build time\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function serialization requires Function type\nexport function serializeFunction(fn: Function): string {\n if (!isDev) {\n // In production, return empty string - this won't be used\n return \"\";\n }\n\n return serialize(fn, { space: 0 });\n}\n\n/**\n * Deserialize a function received from server\n *\n * DEV ONLY: Eval serialized function source\n * PROD: Should never be called - effects come from bundle\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function deserialization requires Function type\nexport function deserializeFunction(serialized: string): Function {\n if (!isDev) {\n throw new Error(\n \"[Polly] deserializeFunction should not be called in production. \" +\n \"Client effects should be imported from your bundle.\"\n );\n }\n\n if (!serialized) {\n throw new Error(\"[Polly] Cannot deserialize empty function\");\n }\n\n // biome-ignore lint/security/noGlobalEval: Required for dev-mode function deserialization\n return eval(`(${serialized})`);\n}\n"
8
+ ],
9
+ "mappings": "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",
10
+ "debugId": "8AEBFE7CEBB3C9C364756E2164756E21",
11
+ "names": []
12
+ }
@@ -0,0 +1,5 @@
1
+ import type { PollyConfig } from "./types";
2
+ /**
3
+ * Main Polly Elysia plugin
4
+ */
5
+ export declare function polly(config?: PollyConfig): any;
@@ -0,0 +1,16 @@
1
+ import type { PollyConfig } from "./types";
2
+ /**
3
+ * Generate TLA+ specification from Elysia app + Polly config
4
+ *
5
+ * This generates a formal model that can be checked with TLC (TLA+ model checker)
6
+ * to verify distributed systems properties like:
7
+ * - Eventually consistent state
8
+ * - Authorization enforcement
9
+ * - No lost updates
10
+ * - Causal ordering of events
11
+ */
12
+ export declare function generateTLASpec(moduleName: string, config: PollyConfig): string;
13
+ /**
14
+ * Export TLA+ spec to file
15
+ */
16
+ export declare function exportTLASpec(moduleName: string, config: PollyConfig, outputPath: string): Promise<void>;
@@ -0,0 +1,137 @@
1
+ import type { Signal } from "@preact/signals-core";
2
+ /**
3
+ * Route pattern matching configuration
4
+ * Examples: 'POST /todos', 'GET /todos/:id', '/todos/*'
5
+ */
6
+ export type RoutePattern = `${string} ${string}` | string;
7
+ /**
8
+ * State configuration for client and server
9
+ */
10
+ export interface PollyStateConfig {
11
+ client?: Record<string, Signal<unknown>>;
12
+ server?: Record<string, Signal<unknown>>;
13
+ }
14
+ /**
15
+ * Effect handler context
16
+ */
17
+ export interface EffectContext<TResult = unknown, TBody = unknown> {
18
+ result: TResult;
19
+ body: TBody;
20
+ state: {
21
+ client: Record<string, Signal<unknown>>;
22
+ server: Record<string, Signal<unknown>>;
23
+ };
24
+ params: Record<string, string>;
25
+ clock: {
26
+ tick: number;
27
+ contextId: string;
28
+ };
29
+ }
30
+ /**
31
+ * Client effect configuration for a route
32
+ */
33
+ export interface ClientEffectConfig {
34
+ /**
35
+ * Client-side effect to run after successful response
36
+ */
37
+ client: (ctx: EffectContext) => void | Promise<void>;
38
+ /**
39
+ * Whether to broadcast this update to all connected clients
40
+ */
41
+ broadcast?: boolean;
42
+ /**
43
+ * Broadcast filter - only send to clients matching this condition
44
+ */
45
+ broadcastFilter?: (clientState: Record<string, Signal<unknown>>) => boolean;
46
+ }
47
+ /**
48
+ * Authorization handler context
49
+ */
50
+ export interface AuthorizationContext {
51
+ state: {
52
+ client: Record<string, Signal<unknown>>;
53
+ server: Record<string, Signal<unknown>>;
54
+ };
55
+ body?: unknown;
56
+ params?: Record<string, string>;
57
+ headers: Record<string, string>;
58
+ }
59
+ /**
60
+ * Authorization handler - return true to allow, false to deny
61
+ */
62
+ export type AuthorizationHandler = (ctx: AuthorizationContext) => boolean | Promise<boolean>;
63
+ /**
64
+ * Offline behavior configuration for a route
65
+ */
66
+ export interface OfflineConfig<TBody = unknown, TResult = unknown> {
67
+ /**
68
+ * Whether to queue this request when offline
69
+ */
70
+ queue: boolean;
71
+ /**
72
+ * Optimistic update - what to show the user immediately
73
+ */
74
+ optimistic?: (body: TBody) => TResult;
75
+ /**
76
+ * Merge strategy when online again
77
+ * - 'replace': Replace optimistic with server result
78
+ * - 'merge': Custom merge function
79
+ */
80
+ merge?: "replace" | ((optimistic: TResult, server: TResult) => TResult);
81
+ /**
82
+ * Conflict resolution if multiple devices edited offline
83
+ */
84
+ conflictResolution?: "last-write-wins" | "server-wins" | ((client: TResult, server: TResult) => TResult);
85
+ }
86
+ /**
87
+ * Complete Polly middleware configuration
88
+ */
89
+ export interface PollyConfig {
90
+ /**
91
+ * State signals (client and server)
92
+ */
93
+ state?: PollyStateConfig;
94
+ /**
95
+ * Client effects mapped to route patterns
96
+ */
97
+ effects?: Record<RoutePattern, ClientEffectConfig>;
98
+ /**
99
+ * Authorization rules mapped to route patterns
100
+ */
101
+ authorization?: Record<RoutePattern, AuthorizationHandler>;
102
+ /**
103
+ * Offline behavior mapped to route patterns
104
+ */
105
+ offline?: Record<RoutePattern, OfflineConfig>;
106
+ /**
107
+ * WebSocket path for real-time updates (default: '/polly/ws')
108
+ */
109
+ websocketPath?: string;
110
+ /**
111
+ * Enable TLA+ generation (default: false)
112
+ */
113
+ tlaGeneration?: boolean;
114
+ }
115
+ /**
116
+ * Metadata added to responses for client wrapper
117
+ */
118
+ export interface PollyResponseMetadata {
119
+ /**
120
+ * Client effect to execute
121
+ */
122
+ clientEffect?: {
123
+ handler: string;
124
+ broadcast: boolean;
125
+ };
126
+ /**
127
+ * Offline configuration for this route
128
+ */
129
+ offline?: OfflineConfig;
130
+ /**
131
+ * Lamport clock info
132
+ */
133
+ clock: {
134
+ tick: number;
135
+ contextId: string;
136
+ };
137
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Serialize a function to send to client
3
+ *
4
+ * DEV ONLY: Used for hot reloading and debugging
5
+ * PROD: No-op - client effects are baked into bundle at build time
6
+ */
7
+ export declare function serializeFunction(fn: Function): string;
8
+ /**
9
+ * Deserialize a function received from server
10
+ *
11
+ * DEV ONLY: Eval serialized function source
12
+ * PROD: Should never be called - effects come from bundle
13
+ */
14
+ export declare function deserializeFunction(serialized: string): Function;
@@ -0,0 +1,37 @@
1
+ import type { ADRCollection } from "../types/adr";
2
+ export declare class ADRExtractor {
3
+ private projectRoot;
4
+ constructor(projectRoot: string);
5
+ /**
6
+ * Extract ADRs from docs/adr directory
7
+ */
8
+ extract(): ADRCollection;
9
+ /**
10
+ * Find ADR directory
11
+ */
12
+ private findADRDirectory;
13
+ /**
14
+ * Parse ADR from markdown file
15
+ */
16
+ private parseADR;
17
+ /**
18
+ * Extract status from content
19
+ */
20
+ private extractStatus;
21
+ /**
22
+ * Extract date from content
23
+ */
24
+ private extractDate;
25
+ /**
26
+ * Extract section content
27
+ */
28
+ private extractSection;
29
+ /**
30
+ * Extract links to other ADRs
31
+ */
32
+ private extractLinks;
33
+ }
34
+ /**
35
+ * Extract ADRs from project
36
+ */
37
+ export declare function extractADRs(projectRoot: string): ADRCollection;