@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.
- package/dist/src/client/index.d.ts +33 -0
- package/dist/src/client/index.js +586 -0
- package/dist/src/client/index.js.map +13 -0
- package/dist/src/client/wrapper.d.ts +54 -0
- package/dist/src/core/clock.d.ts +63 -0
- package/dist/src/elysia/index.d.ts +43 -0
- package/dist/src/elysia/index.js +241 -0
- package/dist/src/elysia/index.js.map +12 -0
- package/dist/src/elysia/plugin.d.ts +5 -0
- package/dist/src/elysia/tla-generator.d.ts +16 -0
- package/dist/src/elysia/types.d.ts +137 -0
- package/dist/src/utils/function-serialization.d.ts +14 -0
- package/dist/tools/analysis/src/extract/adr.d.ts +37 -0
- package/dist/tools/analysis/src/extract/architecture.d.ts +42 -0
- package/dist/tools/analysis/src/extract/contexts.d.ts +74 -0
- package/dist/tools/analysis/src/extract/flows.d.ts +68 -0
- package/dist/tools/analysis/src/extract/handlers.d.ts +330 -0
- package/dist/tools/analysis/src/extract/index.d.ts +9 -0
- package/dist/tools/analysis/src/extract/integrations.d.ts +77 -0
- package/dist/tools/analysis/src/extract/manifest.d.ts +64 -0
- package/dist/tools/analysis/src/extract/project-detector.d.ts +103 -0
- package/dist/tools/analysis/src/extract/relationships.d.ts +119 -0
- package/dist/tools/analysis/src/extract/types.d.ts +139 -0
- package/dist/tools/analysis/src/index.d.ts +2 -0
- package/dist/tools/analysis/src/types/adr.d.ts +39 -0
- package/dist/tools/analysis/src/types/architecture.d.ts +198 -0
- package/dist/tools/analysis/src/types/core.d.ts +178 -0
- package/dist/tools/analysis/src/types/index.d.ts +4 -0
- package/dist/tools/teach/src/cli.js +140 -69
- package/dist/tools/teach/src/cli.js.map +12 -12
- package/dist/tools/teach/src/index.d.ts +28 -0
- package/dist/tools/teach/src/index.js +145 -72
- package/dist/tools/teach/src/index.js.map +13 -13
- package/dist/tools/verify/src/cli.js +33 -11
- package/dist/tools/verify/src/cli.js.map +5 -5
- package/dist/tools/visualize/src/cli.js +125 -66
- package/dist/tools/visualize/src/cli.js.map +11 -11
- package/dist/tools/visualize/src/codegen/structurizr.d.ts +343 -0
- package/dist/tools/visualize/src/types/structurizr.d.ts +235 -0
- 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,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;
|