@fairfox/polly 0.1.1 → 0.1.2
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/cli/polly.ts +9 -3
- package/package.json +2 -2
- package/vendor/analysis/src/extract/adr.ts +212 -0
- package/vendor/analysis/src/extract/architecture.ts +160 -0
- package/vendor/analysis/src/extract/contexts.ts +298 -0
- package/vendor/analysis/src/extract/flows.ts +309 -0
- package/vendor/analysis/src/extract/handlers.ts +321 -0
- package/vendor/analysis/src/extract/index.ts +9 -0
- package/vendor/analysis/src/extract/integrations.ts +329 -0
- package/vendor/analysis/src/extract/manifest.ts +298 -0
- package/vendor/analysis/src/extract/types.ts +389 -0
- package/vendor/analysis/src/index.ts +7 -0
- package/vendor/analysis/src/types/adr.ts +53 -0
- package/vendor/analysis/src/types/architecture.ts +245 -0
- package/vendor/analysis/src/types/core.ts +210 -0
- package/vendor/analysis/src/types/index.ts +18 -0
- package/vendor/verify/src/adapters/base.ts +164 -0
- package/vendor/verify/src/adapters/detection.ts +281 -0
- package/vendor/verify/src/adapters/event-bus/index.ts +480 -0
- package/vendor/verify/src/adapters/web-extension/index.ts +508 -0
- package/vendor/verify/src/adapters/websocket/index.ts +486 -0
- package/vendor/verify/src/cli.ts +430 -0
- package/vendor/verify/src/codegen/config.ts +354 -0
- package/vendor/verify/src/codegen/tla.ts +719 -0
- package/vendor/verify/src/config/parser.ts +303 -0
- package/vendor/verify/src/config/types.ts +113 -0
- package/vendor/verify/src/core/model.ts +267 -0
- package/vendor/verify/src/core/primitives.ts +106 -0
- package/vendor/verify/src/extract/handlers.ts +2 -0
- package/vendor/verify/src/extract/types.ts +2 -0
- package/vendor/verify/src/index.ts +150 -0
- package/vendor/verify/src/primitives/index.ts +102 -0
- package/vendor/verify/src/runner/docker.ts +283 -0
- package/vendor/verify/src/types.ts +51 -0
- package/vendor/visualize/src/cli.ts +365 -0
- package/vendor/visualize/src/codegen/structurizr.ts +770 -0
- package/vendor/visualize/src/index.ts +13 -0
- package/vendor/visualize/src/runner/export.ts +235 -0
- package/vendor/visualize/src/viewer/server.ts +485 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// WebSocket Adapter
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Extracts verification model from Bun WebSocket applications.
|
|
6
|
+
// Recognizes:
|
|
7
|
+
// - WebSocket server/client communication
|
|
8
|
+
// - Eden type definitions for messages
|
|
9
|
+
// - Handler functions (handle* pattern)
|
|
10
|
+
// - State mutations via global state object
|
|
11
|
+
// - Verification primitives (requires, ensures)
|
|
12
|
+
|
|
13
|
+
import { Project, type SourceFile, SyntaxKind, Node } from "ts-morph";
|
|
14
|
+
import type {
|
|
15
|
+
CoreVerificationModel,
|
|
16
|
+
MessageHandler,
|
|
17
|
+
StateAssignment,
|
|
18
|
+
VerificationCondition,
|
|
19
|
+
NodeDefinition,
|
|
20
|
+
MessageType,
|
|
21
|
+
RoutingRule,
|
|
22
|
+
} from "../../core/model";
|
|
23
|
+
import type { AdapterConfig, RoutingAdapter } from "../base";
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────
|
|
26
|
+
// Configuration
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface WebSocketAdapterConfig extends AdapterConfig {
|
|
30
|
+
/** Whether to use Eden types for message extraction */
|
|
31
|
+
useEdenTypes?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Pattern to match handler functions (default: /^handle[A-Z]/) */
|
|
34
|
+
handlerPattern?: RegExp;
|
|
35
|
+
|
|
36
|
+
/** Maximum concurrent connections to model (default: 10) */
|
|
37
|
+
maxConnections?: number;
|
|
38
|
+
|
|
39
|
+
/** Maximum messages in flight (default: 5) */
|
|
40
|
+
maxInFlight?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────
|
|
44
|
+
// WebSocket Adapter Implementation
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export class WebSocketAdapter implements RoutingAdapter<WebSocketAdapterConfig> {
|
|
48
|
+
readonly name = "websocket";
|
|
49
|
+
readonly config: WebSocketAdapterConfig;
|
|
50
|
+
private project: Project;
|
|
51
|
+
|
|
52
|
+
constructor(config: WebSocketAdapterConfig) {
|
|
53
|
+
this.config = {
|
|
54
|
+
useEdenTypes: true,
|
|
55
|
+
handlerPattern: /^handle[A-Z]/,
|
|
56
|
+
maxConnections: 10,
|
|
57
|
+
maxInFlight: 5,
|
|
58
|
+
...config,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this.project = new Project({
|
|
62
|
+
tsConfigFilePath: config.tsConfigPath,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract the complete verification model from the WebSocket application
|
|
68
|
+
*/
|
|
69
|
+
extractModel(): CoreVerificationModel {
|
|
70
|
+
const sourceFiles = this.project.getSourceFiles();
|
|
71
|
+
|
|
72
|
+
// Extract all handlers
|
|
73
|
+
const handlers: MessageHandler[] = [];
|
|
74
|
+
const messageTypeNames = new Set<string>();
|
|
75
|
+
|
|
76
|
+
for (const sourceFile of sourceFiles) {
|
|
77
|
+
const fileHandlers = this.extractHandlersFromFile(sourceFile);
|
|
78
|
+
handlers.push(...fileHandlers);
|
|
79
|
+
|
|
80
|
+
for (const handler of fileHandlers) {
|
|
81
|
+
messageTypeNames.add(handler.messageType);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Define nodes (server + clients)
|
|
86
|
+
const nodes: NodeDefinition[] = [
|
|
87
|
+
{
|
|
88
|
+
id: "server",
|
|
89
|
+
type: "websocket-server",
|
|
90
|
+
canSendTo: ["*"], // Server can send to all clients
|
|
91
|
+
canReceiveFrom: ["*"], // Server receives from all clients
|
|
92
|
+
metadata: {
|
|
93
|
+
isHub: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// Add client nodes (bounded for model checking)
|
|
99
|
+
for (let i = 1; i <= this.config.maxConnections!; i++) {
|
|
100
|
+
nodes.push({
|
|
101
|
+
id: `client-${i}`,
|
|
102
|
+
type: "websocket-client",
|
|
103
|
+
canSendTo: ["server"], // Clients send to server
|
|
104
|
+
canReceiveFrom: ["server"], // Clients receive from server
|
|
105
|
+
metadata: {
|
|
106
|
+
clientId: i,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Define message types
|
|
112
|
+
const messageTypes: MessageType[] = Array.from(messageTypeNames).map((name) => ({
|
|
113
|
+
name,
|
|
114
|
+
payload: {
|
|
115
|
+
name: "unknown",
|
|
116
|
+
kind: "unknown",
|
|
117
|
+
nullable: false,
|
|
118
|
+
},
|
|
119
|
+
routing: {
|
|
120
|
+
from:
|
|
121
|
+
name.startsWith("USER_") ||
|
|
122
|
+
name.startsWith("CHAT_") ||
|
|
123
|
+
name.startsWith("TODO_") ||
|
|
124
|
+
name.startsWith("SYNC_")
|
|
125
|
+
? ["client-*"] // Client messages
|
|
126
|
+
: ["server"], // Server messages
|
|
127
|
+
to:
|
|
128
|
+
name.startsWith("USER_") ||
|
|
129
|
+
name.startsWith("CHAT_") ||
|
|
130
|
+
name.startsWith("TODO_") ||
|
|
131
|
+
name.startsWith("SYNC_")
|
|
132
|
+
? ["server"] // To server
|
|
133
|
+
: ["client-*"], // To clients
|
|
134
|
+
},
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Define routing rules (hub-and-spoke pattern)
|
|
138
|
+
const routingRules: RoutingRule[] = [
|
|
139
|
+
{
|
|
140
|
+
pattern: "request-reply",
|
|
141
|
+
messageTypes: Array.from(messageTypeNames),
|
|
142
|
+
description:
|
|
143
|
+
"WebSocket hub-and-spoke: clients send to server, server broadcasts to clients",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
nodes,
|
|
149
|
+
messageTypes,
|
|
150
|
+
routingRules,
|
|
151
|
+
state: {}, // Populated by user configuration
|
|
152
|
+
handlers,
|
|
153
|
+
bounds: {
|
|
154
|
+
maxConcurrentMessages: this.config.maxInFlight!,
|
|
155
|
+
maxNodes: nodes.length,
|
|
156
|
+
custom: {
|
|
157
|
+
maxConnections: this.config.maxConnections!,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Recognize message handler: handle* functions
|
|
165
|
+
*/
|
|
166
|
+
recognizeMessageHandler(node: Node): MessageHandler | null {
|
|
167
|
+
if (!Node.isFunctionDeclaration(node)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const name = node.getName();
|
|
172
|
+
if (!name || !this.config.handlerPattern!.test(name)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return this.extractHandlerFromFunction(node);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Recognize state mutation: state.field = value
|
|
181
|
+
*/
|
|
182
|
+
recognizeStateUpdate(node: Node): StateAssignment | null {
|
|
183
|
+
if (!Node.isBinaryExpression(node)) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const operator = node.getOperatorToken().getText();
|
|
188
|
+
if (operator !== "=" && operator !== "+=" && operator !== "-=") {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const left = node.getLeft();
|
|
193
|
+
const right = node.getRight();
|
|
194
|
+
|
|
195
|
+
// Check if left side is a state property access
|
|
196
|
+
if (!Node.isPropertyAccessExpression(left)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const fieldPath = this.getPropertyPath(left);
|
|
201
|
+
|
|
202
|
+
// Check if this is a state access
|
|
203
|
+
if (!fieldPath.startsWith("state.")) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const field = fieldPath.substring(6); // Remove "state." prefix
|
|
208
|
+
|
|
209
|
+
// For += and -= operators, we can't extract the exact value
|
|
210
|
+
if (operator !== "=") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const value = this.extractValue(right);
|
|
215
|
+
|
|
216
|
+
if (value === undefined) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
field,
|
|
222
|
+
value,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Recognize verification condition: requires() or ensures()
|
|
228
|
+
*/
|
|
229
|
+
recognizeVerificationCondition(
|
|
230
|
+
node: Node,
|
|
231
|
+
type: "precondition" | "postcondition"
|
|
232
|
+
): VerificationCondition | null {
|
|
233
|
+
if (!Node.isCallExpression(node)) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const callee = node.getExpression();
|
|
238
|
+
if (!Node.isIdentifier(callee)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const functionName = callee.getText();
|
|
243
|
+
const expectedName = type === "precondition" ? "requires" : "ensures";
|
|
244
|
+
|
|
245
|
+
if (functionName !== expectedName) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return this.extractCondition(node);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─────────────────────────────────────────────────────────────────
|
|
253
|
+
// Private Helper Methods
|
|
254
|
+
// ─────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extract all handlers from a source file
|
|
258
|
+
*/
|
|
259
|
+
private extractHandlersFromFile(sourceFile: SourceFile): MessageHandler[] {
|
|
260
|
+
const handlers: MessageHandler[] = [];
|
|
261
|
+
|
|
262
|
+
sourceFile.forEachDescendant((node) => {
|
|
263
|
+
const handler = this.recognizeMessageHandler(node);
|
|
264
|
+
if (handler) {
|
|
265
|
+
handlers.push(handler);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return handlers;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Extract handler details from a handle* function
|
|
274
|
+
*/
|
|
275
|
+
private extractHandlerFromFunction(funcNode: Node): MessageHandler | null {
|
|
276
|
+
if (!Node.isFunctionDeclaration(funcNode)) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const name = funcNode.getName();
|
|
281
|
+
if (!name) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Extract message type from function name
|
|
286
|
+
// handleUserJoin -> USER_JOIN
|
|
287
|
+
const messageType = this.functionNameToMessageType(name);
|
|
288
|
+
|
|
289
|
+
const assignments: StateAssignment[] = [];
|
|
290
|
+
const preconditions: VerificationCondition[] = [];
|
|
291
|
+
const postconditions: VerificationCondition[] = [];
|
|
292
|
+
|
|
293
|
+
// Parse the function body
|
|
294
|
+
this.extractAssignmentsFromFunction(funcNode, assignments);
|
|
295
|
+
this.extractVerificationConditionsFromFunction(funcNode, preconditions, postconditions);
|
|
296
|
+
|
|
297
|
+
const sourceFile = funcNode.getSourceFile();
|
|
298
|
+
const line = funcNode.getStartLineNumber();
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
messageType,
|
|
302
|
+
node: "server", // All handlers run on server
|
|
303
|
+
assignments,
|
|
304
|
+
preconditions,
|
|
305
|
+
postconditions,
|
|
306
|
+
location: {
|
|
307
|
+
file: sourceFile.getFilePath(),
|
|
308
|
+
line,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Convert handler function name to message type
|
|
315
|
+
* handleUserJoin -> USER_JOIN
|
|
316
|
+
*/
|
|
317
|
+
private functionNameToMessageType(name: string): string {
|
|
318
|
+
// Remove "handle" prefix
|
|
319
|
+
const withoutHandle = name.replace(/^handle/, "");
|
|
320
|
+
|
|
321
|
+
// Convert PascalCase to SCREAMING_SNAKE_CASE
|
|
322
|
+
return withoutHandle
|
|
323
|
+
.replace(/([A-Z])/g, "_$1")
|
|
324
|
+
.replace(/^_/, "")
|
|
325
|
+
.toUpperCase();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Extract state assignments from a function
|
|
330
|
+
*/
|
|
331
|
+
private extractAssignmentsFromFunction(funcNode: Node, assignments: StateAssignment[]): void {
|
|
332
|
+
funcNode.forEachDescendant((node) => {
|
|
333
|
+
const assignment = this.recognizeStateUpdate(node);
|
|
334
|
+
if (assignment) {
|
|
335
|
+
assignments.push(assignment);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Extract verification conditions from a function
|
|
342
|
+
*/
|
|
343
|
+
private extractVerificationConditionsFromFunction(
|
|
344
|
+
funcNode: Node,
|
|
345
|
+
preconditions: VerificationCondition[],
|
|
346
|
+
postconditions: VerificationCondition[]
|
|
347
|
+
): void {
|
|
348
|
+
if (!Node.isFunctionDeclaration(funcNode)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const body = funcNode.getBody();
|
|
353
|
+
|
|
354
|
+
if (!body || !Node.isBlock(body)) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Get all statements in the function body
|
|
359
|
+
const statements = body.getStatements();
|
|
360
|
+
|
|
361
|
+
for (const statement of statements) {
|
|
362
|
+
// Look for expression statements that are function calls
|
|
363
|
+
if (Node.isExpressionStatement(statement)) {
|
|
364
|
+
const expr = statement.getExpression();
|
|
365
|
+
|
|
366
|
+
const precond = this.recognizeVerificationCondition(expr, "precondition");
|
|
367
|
+
if (precond) {
|
|
368
|
+
preconditions.push(precond);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const postcond = this.recognizeVerificationCondition(expr, "postcondition");
|
|
372
|
+
if (postcond) {
|
|
373
|
+
postconditions.push(postcond);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Extract condition from a requires() or ensures() call
|
|
381
|
+
*/
|
|
382
|
+
private extractCondition(callExpr: Node): VerificationCondition | null {
|
|
383
|
+
if (!Node.isCallExpression(callExpr)) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const args = callExpr.getArguments();
|
|
388
|
+
|
|
389
|
+
if (args.length === 0) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// First argument is the condition expression
|
|
394
|
+
const conditionArg = args[0];
|
|
395
|
+
const expression = conditionArg.getText();
|
|
396
|
+
|
|
397
|
+
// Second argument (optional) is the message
|
|
398
|
+
let message: string | undefined;
|
|
399
|
+
if (args.length >= 2 && Node.isStringLiteral(args[1])) {
|
|
400
|
+
message = args[1].getLiteralValue();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const line = callExpr.getStartLineNumber();
|
|
404
|
+
const column = callExpr.getStart();
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
expression,
|
|
408
|
+
message,
|
|
409
|
+
location: {
|
|
410
|
+
line,
|
|
411
|
+
column,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get the full property access path (e.g., "state.user.loggedIn")
|
|
418
|
+
*/
|
|
419
|
+
private getPropertyPath(node: Node): string {
|
|
420
|
+
const parts: string[] = [];
|
|
421
|
+
|
|
422
|
+
let current: Node = node;
|
|
423
|
+
while (Node.isPropertyAccessExpression(current)) {
|
|
424
|
+
parts.unshift(current.getName());
|
|
425
|
+
current = current.getExpression();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Add the base identifier
|
|
429
|
+
if (Node.isIdentifier(current)) {
|
|
430
|
+
parts.unshift(current.getText());
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return parts.join(".");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Extract a literal value from an expression
|
|
438
|
+
*/
|
|
439
|
+
private extractValue(node: Node): string | boolean | number | null | undefined {
|
|
440
|
+
if (Node.isStringLiteral(node)) {
|
|
441
|
+
return node.getLiteralValue();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (Node.isNumericLiteral(node)) {
|
|
445
|
+
return node.getLiteralValue();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (node.getKind() === SyntaxKind.TrueKeyword) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (node.getKind() === SyntaxKind.FalseKeyword) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (node.getKind() === SyntaxKind.NullKeyword) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// For complex expressions, return undefined (can't extract)
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Custom invariants specific to WebSocket systems
|
|
466
|
+
*/
|
|
467
|
+
customInvariants(): Array<[name: string, tlaExpression: string]> {
|
|
468
|
+
return [
|
|
469
|
+
[
|
|
470
|
+
"ServerAlwaysAvailable",
|
|
471
|
+
'ports["server"] = "connected" \\* Server must always be available',
|
|
472
|
+
],
|
|
473
|
+
[
|
|
474
|
+
"ClientsConnectToServer",
|
|
475
|
+
"\\A msg \\in Range(messages) : " +
|
|
476
|
+
'(msg.source # "server") => ("server" \\in msg.targets) \\* Clients must route through server',
|
|
477
|
+
],
|
|
478
|
+
[
|
|
479
|
+
"BroadcastConsistency",
|
|
480
|
+
"\\A c1, c2 \\in Contexts : " +
|
|
481
|
+
'(c1 # "server" /\\ c2 # "server" /\\ ports[c1] = "connected" /\\ ports[c2] = "connected") => ' +
|
|
482
|
+
"\\* All connected clients eventually receive broadcasts",
|
|
483
|
+
],
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
}
|