@fairfox/polly 0.14.0 → 0.15.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/background/index.js +342 -3
- package/dist/src/background/index.js.map +7 -4
- package/dist/src/background/message-router.js +342 -3
- package/dist/src/background/message-router.js.map +7 -4
- package/dist/src/index.js +402 -99
- package/dist/src/index.js.map +8 -5
- package/dist/src/shared/adapters/index.d.ts +3 -0
- package/dist/src/shared/adapters/index.js +356 -4
- package/dist/src/shared/adapters/index.js.map +7 -4
- package/dist/src/shared/lib/adapter-factory.d.ts +80 -0
- package/dist/src/shared/lib/context-helpers.js +342 -3
- package/dist/src/shared/lib/context-helpers.js.map +7 -4
- package/dist/src/shared/lib/message-bus.js +342 -3
- package/dist/src/shared/lib/message-bus.js.map +7 -4
- package/dist/src/shared/lib/state.d.ts +5 -1
- package/dist/src/shared/lib/state.js +274 -1173
- package/dist/src/shared/lib/state.js.map +6 -19
- package/dist/src/shared/lib/storage-adapter.d.ts +42 -0
- package/dist/src/shared/lib/sync-adapter.d.ts +79 -0
- package/dist/src/shared/state/app-state.js +294 -1173
- package/dist/src/shared/state/app-state.js.map +6 -18
- package/dist/tools/analysis/src/extract/handlers.d.ts +48 -1
- package/dist/tools/analysis/src/types/core.d.ts +20 -0
- package/dist/tools/teach/src/cli.js +454 -7
- package/dist/tools/teach/src/cli.js.map +4 -4
- package/dist/tools/teach/src/index.js +232 -7
- package/dist/tools/teach/src/index.js.map +3 -3
- package/dist/tools/verify/src/cli.js +234 -9
- package/dist/tools/verify/src/cli.js.map +4 -4
- package/dist/tools/visualize/src/cli.js +232 -7
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +1 -1
|
@@ -5502,21 +5502,45 @@ class HandlerExtractor {
|
|
|
5502
5502
|
const messageTypes = new Set;
|
|
5503
5503
|
const invalidMessageTypes = new Set;
|
|
5504
5504
|
const stateConstraints = [];
|
|
5505
|
+
const verifiedStates = [];
|
|
5505
5506
|
const allSourceFiles = this.project.getSourceFiles();
|
|
5506
5507
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
5507
5508
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
5508
5509
|
for (const entryPoint of entryPoints) {
|
|
5509
|
-
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints);
|
|
5510
|
+
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
5511
|
+
}
|
|
5512
|
+
if (verifiedStates.length > 0) {
|
|
5513
|
+
if (process.env["POLLY_DEBUG"]) {
|
|
5514
|
+
console.log(`[DEBUG] Found ${verifiedStates.length} verified state(s), scanning for mutating functions...`);
|
|
5515
|
+
}
|
|
5516
|
+
for (const filePath of this.analyzedFiles) {
|
|
5517
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
5518
|
+
if (!sourceFile)
|
|
5519
|
+
continue;
|
|
5520
|
+
const mutatingHandlers = this.findStateMutatingFunctions(sourceFile, verifiedStates);
|
|
5521
|
+
for (const handler of mutatingHandlers) {
|
|
5522
|
+
const exists = handlers.some((h) => h.messageType === handler.messageType && h.location.file === handler.location.file);
|
|
5523
|
+
if (!exists) {
|
|
5524
|
+
handlers.push(handler);
|
|
5525
|
+
if (this.isValidTLAIdentifier(handler.messageType)) {
|
|
5526
|
+
messageTypes.add(handler.messageType);
|
|
5527
|
+
} else {
|
|
5528
|
+
invalidMessageTypes.add(handler.messageType);
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5510
5533
|
}
|
|
5511
5534
|
this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
|
|
5512
5535
|
this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
|
|
5513
5536
|
return {
|
|
5514
5537
|
handlers,
|
|
5515
5538
|
messageTypes,
|
|
5516
|
-
stateConstraints
|
|
5539
|
+
stateConstraints,
|
|
5540
|
+
verifiedStates
|
|
5517
5541
|
};
|
|
5518
5542
|
}
|
|
5519
|
-
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints) {
|
|
5543
|
+
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
|
|
5520
5544
|
const filePath = sourceFile.getFilePath();
|
|
5521
5545
|
if (this.analyzedFiles.has(filePath)) {
|
|
5522
5546
|
return;
|
|
@@ -5530,6 +5554,8 @@ class HandlerExtractor {
|
|
|
5530
5554
|
this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
|
|
5531
5555
|
const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
|
|
5532
5556
|
stateConstraints.push(...fileConstraints);
|
|
5557
|
+
const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
|
|
5558
|
+
verifiedStates.push(...fileVerifiedStates);
|
|
5533
5559
|
const importDeclarations = sourceFile.getImportDeclarations();
|
|
5534
5560
|
for (const importDecl of importDeclarations) {
|
|
5535
5561
|
const importedFile = importDecl.getModuleSpecifierSourceFile();
|
|
@@ -5541,7 +5567,7 @@ class HandlerExtractor {
|
|
|
5541
5567
|
}
|
|
5542
5568
|
continue;
|
|
5543
5569
|
}
|
|
5544
|
-
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints);
|
|
5570
|
+
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
5545
5571
|
} else if (process.env["POLLY_DEBUG"]) {
|
|
5546
5572
|
const specifier = importDecl.getModuleSpecifierValue();
|
|
5547
5573
|
if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
|
|
@@ -5750,7 +5776,7 @@ class HandlerExtractor {
|
|
|
5750
5776
|
return;
|
|
5751
5777
|
}
|
|
5752
5778
|
const valueMatch = fieldPath.match(/\.value\.(.+)$/);
|
|
5753
|
-
if (valueMatch
|
|
5779
|
+
if (valueMatch?.[1]) {
|
|
5754
5780
|
const field = valueMatch[1];
|
|
5755
5781
|
const value = this.extractValue(right);
|
|
5756
5782
|
if (value !== undefined) {
|
|
@@ -5808,7 +5834,7 @@ class HandlerExtractor {
|
|
|
5808
5834
|
return fieldPath.substring(6);
|
|
5809
5835
|
}
|
|
5810
5836
|
const valueMatch = fieldPath.match(/\.value\.(.+)$/);
|
|
5811
|
-
if (valueMatch
|
|
5837
|
+
if (valueMatch?.[1]) {
|
|
5812
5838
|
return valueMatch[1];
|
|
5813
5839
|
}
|
|
5814
5840
|
return null;
|
|
@@ -6616,6 +6642,205 @@ class HandlerExtractor {
|
|
|
6616
6642
|
}
|
|
6617
6643
|
return results;
|
|
6618
6644
|
}
|
|
6645
|
+
extractVerifiedStatesFromFile(sourceFile) {
|
|
6646
|
+
const verifiedStates = [];
|
|
6647
|
+
const filePath = sourceFile.getFilePath();
|
|
6648
|
+
sourceFile.forEachDescendant((node) => {
|
|
6649
|
+
if (!Node4.isCallExpression(node))
|
|
6650
|
+
return;
|
|
6651
|
+
const stateInfo = this.recognizeVerifiedStateCall(node, filePath);
|
|
6652
|
+
if (stateInfo) {
|
|
6653
|
+
verifiedStates.push(stateInfo);
|
|
6654
|
+
}
|
|
6655
|
+
});
|
|
6656
|
+
return verifiedStates;
|
|
6657
|
+
}
|
|
6658
|
+
recognizeVerifiedStateCall(node, filePath) {
|
|
6659
|
+
if (!Node4.isCallExpression(node))
|
|
6660
|
+
return null;
|
|
6661
|
+
const expression = node.getExpression();
|
|
6662
|
+
if (!Node4.isIdentifier(expression))
|
|
6663
|
+
return null;
|
|
6664
|
+
const funcName = expression.getText();
|
|
6665
|
+
if (!["$sharedState", "$syncedState", "$persistedState"].includes(funcName)) {
|
|
6666
|
+
return null;
|
|
6667
|
+
}
|
|
6668
|
+
const args = node.getArguments();
|
|
6669
|
+
if (args.length < 2)
|
|
6670
|
+
return null;
|
|
6671
|
+
const optionsArg = args[2];
|
|
6672
|
+
if (!optionsArg || !this.hasVerifyTrue(optionsArg))
|
|
6673
|
+
return null;
|
|
6674
|
+
const keyArg = args[0];
|
|
6675
|
+
if (!keyArg || !Node4.isStringLiteral(keyArg))
|
|
6676
|
+
return null;
|
|
6677
|
+
const key = keyArg.getLiteralValue();
|
|
6678
|
+
const variableName = this.getVariableNameFromParent(node) || key;
|
|
6679
|
+
const initialValueArg = args[1];
|
|
6680
|
+
const fields = initialValueArg ? this.extractFieldNames(initialValueArg) : [];
|
|
6681
|
+
if (process.env["POLLY_DEBUG"]) {
|
|
6682
|
+
console.log(`[DEBUG] Found verified state: ${variableName} (key: "${key}") with fields: [${fields.join(", ")}]`);
|
|
6683
|
+
}
|
|
6684
|
+
return {
|
|
6685
|
+
key,
|
|
6686
|
+
variableName,
|
|
6687
|
+
filePath,
|
|
6688
|
+
line: node.getStartLineNumber(),
|
|
6689
|
+
fields
|
|
6690
|
+
};
|
|
6691
|
+
}
|
|
6692
|
+
hasVerifyTrue(optionsNode) {
|
|
6693
|
+
if (!Node4.isObjectLiteralExpression(optionsNode))
|
|
6694
|
+
return false;
|
|
6695
|
+
for (const prop of optionsNode.getProperties()) {
|
|
6696
|
+
if (!Node4.isPropertyAssignment(prop))
|
|
6697
|
+
continue;
|
|
6698
|
+
const name = prop.getName();
|
|
6699
|
+
if (name !== "verify")
|
|
6700
|
+
continue;
|
|
6701
|
+
const initializer = prop.getInitializer();
|
|
6702
|
+
if (initializer && initializer.getKind() === SyntaxKind.TrueKeyword) {
|
|
6703
|
+
return true;
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
return false;
|
|
6707
|
+
}
|
|
6708
|
+
getVariableNameFromParent(node) {
|
|
6709
|
+
const parent = node.getParent();
|
|
6710
|
+
if (Node4.isVariableDeclaration(parent)) {
|
|
6711
|
+
return parent.getName();
|
|
6712
|
+
}
|
|
6713
|
+
return null;
|
|
6714
|
+
}
|
|
6715
|
+
extractFieldNames(node) {
|
|
6716
|
+
const fields = [];
|
|
6717
|
+
if (Node4.isObjectLiteralExpression(node)) {
|
|
6718
|
+
for (const prop of node.getProperties()) {
|
|
6719
|
+
if (Node4.isPropertyAssignment(prop) || Node4.isShorthandPropertyAssignment(prop)) {
|
|
6720
|
+
fields.push(prop.getName());
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
} else if (Node4.isIdentifier(node)) {
|
|
6724
|
+
const definitions = node.getDefinitionNodes();
|
|
6725
|
+
for (const def of definitions) {
|
|
6726
|
+
if (Node4.isVariableDeclaration(def)) {
|
|
6727
|
+
const initializer = def.getInitializer();
|
|
6728
|
+
if (initializer && Node4.isObjectLiteralExpression(initializer)) {
|
|
6729
|
+
return this.extractFieldNames(initializer);
|
|
6730
|
+
}
|
|
6731
|
+
}
|
|
6732
|
+
}
|
|
6733
|
+
}
|
|
6734
|
+
return fields;
|
|
6735
|
+
}
|
|
6736
|
+
findStateMutatingFunctions(sourceFile, verifiedStates) {
|
|
6737
|
+
const handlers = [];
|
|
6738
|
+
const stateVarNames = new Set(verifiedStates.map((s) => s.variableName));
|
|
6739
|
+
const filePath = sourceFile.getFilePath();
|
|
6740
|
+
const context = this.inferContext(filePath);
|
|
6741
|
+
for (const func of sourceFile.getFunctions()) {
|
|
6742
|
+
if (!func.isExported())
|
|
6743
|
+
continue;
|
|
6744
|
+
const funcName = func.getName();
|
|
6745
|
+
if (!funcName)
|
|
6746
|
+
continue;
|
|
6747
|
+
const assignments = this.findStateMutationsInFunction(func, stateVarNames);
|
|
6748
|
+
if (assignments.length === 0)
|
|
6749
|
+
continue;
|
|
6750
|
+
const preconditions = [];
|
|
6751
|
+
const postconditions = [];
|
|
6752
|
+
this.extractVerificationConditions(func, preconditions, postconditions);
|
|
6753
|
+
const messageType = this.functionNameToMessageType(funcName);
|
|
6754
|
+
if (process.env["POLLY_DEBUG"]) {
|
|
6755
|
+
console.log(`[DEBUG] Found state-mutating function: ${funcName} → ${messageType} ` + `(${assignments.length} assignments, ${preconditions.length} preconditions, ${postconditions.length} postconditions)`);
|
|
6756
|
+
}
|
|
6757
|
+
handlers.push({
|
|
6758
|
+
messageType,
|
|
6759
|
+
node: context,
|
|
6760
|
+
assignments,
|
|
6761
|
+
preconditions,
|
|
6762
|
+
postconditions,
|
|
6763
|
+
location: {
|
|
6764
|
+
file: filePath,
|
|
6765
|
+
line: func.getStartLineNumber()
|
|
6766
|
+
}
|
|
6767
|
+
});
|
|
6768
|
+
}
|
|
6769
|
+
for (const varStmt of sourceFile.getVariableStatements()) {
|
|
6770
|
+
if (!varStmt.isExported())
|
|
6771
|
+
continue;
|
|
6772
|
+
for (const decl of varStmt.getDeclarations()) {
|
|
6773
|
+
const initializer = decl.getInitializer();
|
|
6774
|
+
if (!initializer)
|
|
6775
|
+
continue;
|
|
6776
|
+
if (!Node4.isArrowFunction(initializer) && !Node4.isFunctionExpression(initializer))
|
|
6777
|
+
continue;
|
|
6778
|
+
const funcName = decl.getName();
|
|
6779
|
+
if (!funcName)
|
|
6780
|
+
continue;
|
|
6781
|
+
const assignments = this.findStateMutationsInFunction(initializer, stateVarNames);
|
|
6782
|
+
if (assignments.length === 0)
|
|
6783
|
+
continue;
|
|
6784
|
+
const preconditions = [];
|
|
6785
|
+
const postconditions = [];
|
|
6786
|
+
this.extractVerificationConditions(initializer, preconditions, postconditions);
|
|
6787
|
+
const messageType = this.functionNameToMessageType(funcName);
|
|
6788
|
+
if (process.env["POLLY_DEBUG"]) {
|
|
6789
|
+
console.log(`[DEBUG] Found state-mutating arrow function: ${funcName} → ${messageType}`);
|
|
6790
|
+
}
|
|
6791
|
+
handlers.push({
|
|
6792
|
+
messageType,
|
|
6793
|
+
node: context,
|
|
6794
|
+
assignments,
|
|
6795
|
+
preconditions,
|
|
6796
|
+
postconditions,
|
|
6797
|
+
location: {
|
|
6798
|
+
file: filePath,
|
|
6799
|
+
line: decl.getStartLineNumber()
|
|
6800
|
+
}
|
|
6801
|
+
});
|
|
6802
|
+
}
|
|
6803
|
+
}
|
|
6804
|
+
return handlers;
|
|
6805
|
+
}
|
|
6806
|
+
findStateMutationsInFunction(func, stateVarNames) {
|
|
6807
|
+
const mutations = [];
|
|
6808
|
+
func.forEachDescendant((node) => {
|
|
6809
|
+
if (!Node4.isBinaryExpression(node))
|
|
6810
|
+
return;
|
|
6811
|
+
const operator = node.getOperatorToken().getText();
|
|
6812
|
+
if (operator !== "=")
|
|
6813
|
+
return;
|
|
6814
|
+
const left = node.getLeft();
|
|
6815
|
+
if (!Node4.isPropertyAccessExpression(left))
|
|
6816
|
+
return;
|
|
6817
|
+
const path3 = this.getPropertyPath(left);
|
|
6818
|
+
for (const varName of stateVarNames) {
|
|
6819
|
+
if (path3 === `${varName}.value`) {
|
|
6820
|
+
const right = node.getRight();
|
|
6821
|
+
if (Node4.isObjectLiteralExpression(right)) {
|
|
6822
|
+
this.extractObjectLiteralAssignments(right, mutations);
|
|
6823
|
+
}
|
|
6824
|
+
break;
|
|
6825
|
+
}
|
|
6826
|
+
const fieldPrefix = `${varName}.value.`;
|
|
6827
|
+
if (path3.startsWith(fieldPrefix)) {
|
|
6828
|
+
const field = path3.substring(fieldPrefix.length);
|
|
6829
|
+
const value = this.extractValue(node.getRight());
|
|
6830
|
+
mutations.push({ field, value: value ?? "@" });
|
|
6831
|
+
break;
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
});
|
|
6835
|
+
return mutations;
|
|
6836
|
+
}
|
|
6837
|
+
functionNameToMessageType(funcName) {
|
|
6838
|
+
let name = funcName.replace(/^handle/, "").replace(/^on/, "").replace(/^set/, "Set").replace(/^update/, "Update").replace(/^do/, "");
|
|
6839
|
+
if (name.length > 0) {
|
|
6840
|
+
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
6841
|
+
}
|
|
6842
|
+
return name || funcName;
|
|
6843
|
+
}
|
|
6619
6844
|
}
|
|
6620
6845
|
|
|
6621
6846
|
// tools/analysis/src/extract/integrations.ts
|
|
@@ -9080,9 +9305,11 @@ ${generateVerificationSection(context)}
|
|
|
9080
9305
|
- **Translation**: How TypeScript code becomes TLA+ specifications
|
|
9081
9306
|
- **Verification**: What properties are being verified and what they mean
|
|
9082
9307
|
- **State-Level Constraints**: Using $constraints() to declare verification constraints alongside state
|
|
9308
|
+
- **Verified State Discovery**: Using \`{ verify: true }\` on $sharedState to enable automatic handler discovery
|
|
9083
9309
|
- **Performance**: How to optimize verification speed and state space exploration
|
|
9084
9310
|
- **Debugging**: Interpreting counterexamples and fixing violations
|
|
9085
9311
|
- **Configuration**: Understanding maxInFlight, bounds, and other verification parameters
|
|
9312
|
+
- **Universal State Management**: Using the same state code in Chrome extensions, web apps, and Node.js
|
|
9086
9313
|
- **Elysia Integration**: Using Polly with Elysia/Bun servers for full-stack distributed systems verification
|
|
9087
9314
|
- **Testing & Adapters**: Using mock adapters for testing and verification in Node.js environments
|
|
9088
9315
|
|
|
@@ -9101,6 +9328,49 @@ ${generateVerificationSection(context)}
|
|
|
9101
9328
|
- **Transitive Discovery**: The analyzer uses transitive import following to discover constraints
|
|
9102
9329
|
- Files outside src/ are automatically found if imported from handler files
|
|
9103
9330
|
- This enables clean separation of verification code from runtime code
|
|
9331
|
+
- **Verified State Discovery**: When \`{ verify: true }\` is set on $sharedState/$syncedState/$persistedState:
|
|
9332
|
+
- The analyzer automatically discovers exported functions that modify that state signal
|
|
9333
|
+
- Functions with \`requires()\` and \`ensures()\` annotations get those extracted as pre/postconditions
|
|
9334
|
+
- State assignments like \`authState.value = { ... }\` or \`authState.value.field = x\` are detected
|
|
9335
|
+
- Message types are derived from function names: \`handleAuthSuccess\` → \`AuthSuccess\`
|
|
9336
|
+
- This enables verification for applications that don't use \`messageBus.on()\` handlers
|
|
9337
|
+
- Ideal for: multi-tab PWAs, WebSocket apps, reactive effect-driven architectures
|
|
9338
|
+
- Example:
|
|
9339
|
+
\`\`\`typescript
|
|
9340
|
+
export const authState = $sharedState('auth', { isAuthenticated: false }, { verify: true });
|
|
9341
|
+
|
|
9342
|
+
export function handleLogin(): void {
|
|
9343
|
+
requires(!authState.value.isAuthenticated);
|
|
9344
|
+
authState.value = { ...authState.value, isAuthenticated: true };
|
|
9345
|
+
ensures(authState.value.isAuthenticated);
|
|
9346
|
+
}
|
|
9347
|
+
\`\`\`
|
|
9348
|
+
|
|
9349
|
+
# Verification Parameters Explained
|
|
9350
|
+
|
|
9351
|
+
When explaining verification configuration, help users understand what each parameter controls:
|
|
9352
|
+
|
|
9353
|
+
**maxDepth**: Maximum number of **sequential** message deliveries to check
|
|
9354
|
+
- Depth 4 = check sequences up to 4 messages delivered one after another
|
|
9355
|
+
- Depth 8 = check sequences up to 8 messages delivered one after another
|
|
9356
|
+
- Question to ask: "What's the longest sequence of messages that could expose a bug in my app?"
|
|
9357
|
+
- Most authentication/state machine bugs appear in shallow sequences (depth 2-4)
|
|
9358
|
+
|
|
9359
|
+
**maxInFlight**: Maximum number of **concurrent** messages in the system at once
|
|
9360
|
+
- 2 = check scenarios with up to 2 messages pending simultaneously
|
|
9361
|
+
- 3 = check scenarios with up to 3 messages pending simultaneously
|
|
9362
|
+
- Question: "How many concurrent messages can actually happen in my app?"
|
|
9363
|
+
- Consider different bounds per message type (auth should be 1, queries could be higher)
|
|
9364
|
+
|
|
9365
|
+
**maxTabs**: Number of concurrent contexts (tabs, workers, etc.)
|
|
9366
|
+
- Question: "How many concurrent contexts do I need to verify for race conditions?"
|
|
9367
|
+
- Most single-tab apps only need 1; multi-tab/worker apps need 2+
|
|
9368
|
+
|
|
9369
|
+
**Practical Guidance**:
|
|
9370
|
+
- Start conservative: maxDepth: 3-4, maxInFlight: 2, maxTabs: 1
|
|
9371
|
+
- Use timeouts (5-10 minutes) to prevent runaway verification
|
|
9372
|
+
- Don't arbitrarily exclude message types to speed up verification - tune the bounds instead
|
|
9373
|
+
- If verification takes hours, bounds are likely too high for regular development workflows
|
|
9104
9374
|
|
|
9105
9375
|
# Elysia/Bun Integration
|
|
9106
9376
|
|
|
@@ -9150,6 +9420,8 @@ const app = new Elysia()
|
|
|
9150
9420
|
'POST /todos': {
|
|
9151
9421
|
queue: true,
|
|
9152
9422
|
optimistic: (body) => ({ id: -Date.now(), ...body }),
|
|
9423
|
+
merge: 'replace', // 'replace' | custom merge function
|
|
9424
|
+
conflictResolution: 'last-write-wins', // 'last-write-wins' | 'server-wins' | custom function
|
|
9153
9425
|
},
|
|
9154
9426
|
},
|
|
9155
9427
|
}))
|
|
@@ -9166,6 +9438,30 @@ const app = new Elysia()
|
|
|
9166
9438
|
- In production: Pass-through (minimal overhead) - client effects are bundled at build time
|
|
9167
9439
|
- Authorization and broadcasts work in both modes
|
|
9168
9440
|
|
|
9441
|
+
**Offline Behavior Configuration:**
|
|
9442
|
+
|
|
9443
|
+
The \`offline\` config enables Progressive Web App capabilities:
|
|
9444
|
+
|
|
9445
|
+
\`\`\`typescript
|
|
9446
|
+
offline: {
|
|
9447
|
+
'POST /todos': {
|
|
9448
|
+
queue: true, // Queue request when offline
|
|
9449
|
+
optimistic: (body) => TResult, // Immediate UI update with predicted result
|
|
9450
|
+
merge: 'replace' | mergeFn, // How to merge optimistic with server result
|
|
9451
|
+
conflictResolution: 'last-write-wins' | 'server-wins' | resolveFn,
|
|
9452
|
+
},
|
|
9453
|
+
}
|
|
9454
|
+
\`\`\`
|
|
9455
|
+
|
|
9456
|
+
**Merge Strategies:**
|
|
9457
|
+
- \`'replace'\`: Replace optimistic result with server result (default)
|
|
9458
|
+
- \`(optimistic, server) => TResult\`: Custom merge logic
|
|
9459
|
+
|
|
9460
|
+
**Conflict Resolution** (when multiple devices edit offline):
|
|
9461
|
+
- \`'last-write-wins'\`: Lamport clock determines winner
|
|
9462
|
+
- \`'server-wins'\`: Server state always takes precedence
|
|
9463
|
+
- \`(client, server) => TResult\`: Custom conflict resolution
|
|
9464
|
+
|
|
9169
9465
|
## Client-Side Wrapper (\`@fairfox/polly/client\`)
|
|
9170
9466
|
|
|
9171
9467
|
Enhances Eden treaty client with Polly features:
|
|
@@ -9382,6 +9678,126 @@ All context creation functions now follow the adapter pattern:
|
|
|
9382
9678
|
- \`getMessageBus(context, adapters?)\` ✓
|
|
9383
9679
|
- \`new MessageBus(context, adapters?)\` ✓
|
|
9384
9680
|
|
|
9681
|
+
# Universal State Management
|
|
9682
|
+
|
|
9683
|
+
Polly's state primitives (\`$sharedState\`, \`$syncedState\`, \`$persistedState\`, \`$state\`) now work universally across Chrome extensions, web applications/PWAs, and Node.js environments using an **adapter pattern**.
|
|
9684
|
+
|
|
9685
|
+
## The Adapter System
|
|
9686
|
+
|
|
9687
|
+
State management is decoupled into two independent concerns via adapters:
|
|
9688
|
+
|
|
9689
|
+
1. **Storage Adapter** - Where state persists (chrome.storage, IndexedDB, or in-memory)
|
|
9690
|
+
2. **Sync Adapter** - How state synchronizes across contexts (chrome.runtime, BroadcastChannel, or NoOp)
|
|
9691
|
+
|
|
9692
|
+
## Automatic Environment Detection
|
|
9693
|
+
|
|
9694
|
+
The framework automatically detects the environment and selects appropriate adapters:
|
|
9695
|
+
|
|
9696
|
+
| Environment | Storage | Sync Transport | Use Case |
|
|
9697
|
+
|------------|---------|----------------|----------|
|
|
9698
|
+
| **Chrome Extension** | \`chrome.storage.local\` | \`chrome.runtime\` messaging | Multi-context extensions |
|
|
9699
|
+
| **Web App / PWA** | IndexedDB | BroadcastChannel | Multi-tab web applications |
|
|
9700
|
+
| **Single-Tab Web App** | IndexedDB | None (NoOp) | Single-tab applications |
|
|
9701
|
+
| **Node.js / Testing** | In-memory Map | None (NoOp) | Verification & unit tests |
|
|
9702
|
+
|
|
9703
|
+
**Example:**
|
|
9704
|
+
\`\`\`typescript
|
|
9705
|
+
// Same code works everywhere!
|
|
9706
|
+
import { $sharedState } from '@fairfox/polly/state';
|
|
9707
|
+
|
|
9708
|
+
const settings = $sharedState('settings', { theme: 'dark' });
|
|
9709
|
+
|
|
9710
|
+
// In Chrome extension:
|
|
9711
|
+
// → Uses chrome.storage.local + chrome.runtime messaging
|
|
9712
|
+
// In web app:
|
|
9713
|
+
// → Uses IndexedDB + BroadcastChannel
|
|
9714
|
+
// In Node.js test:
|
|
9715
|
+
// → Uses in-memory Map + NoOp sync
|
|
9716
|
+
\`\`\`
|
|
9717
|
+
|
|
9718
|
+
## Why BroadcastChannel for Web Apps?
|
|
9719
|
+
|
|
9720
|
+
**Architecture Decision**: BroadcastChannel was chosen over SharedWorker for web app sync because:
|
|
9721
|
+
- Simpler API with no lifecycle management complexity
|
|
9722
|
+
- Decentralized (aligns with local-first/offline-first architecture)
|
|
9723
|
+
- Better browser support (especially Safari and mobile)
|
|
9724
|
+
- Perfect for message-passing with Lamport clock conflict resolution
|
|
9725
|
+
- No single point of failure
|
|
9726
|
+
|
|
9727
|
+
SharedWorker could be added as an optional adapter in the future for use cases requiring:
|
|
9728
|
+
- Central coordination point for complex multi-tab workflows
|
|
9729
|
+
- Shared WebSocket connections (one connection for all tabs)
|
|
9730
|
+
- Heavy computation done once and shared across tabs
|
|
9731
|
+
- Persistent background work when tabs are closed
|
|
9732
|
+
|
|
9733
|
+
See \`src/shared/lib/sync-adapter.ts\` for detailed architectural rationale.
|
|
9734
|
+
|
|
9735
|
+
## Available Adapters
|
|
9736
|
+
|
|
9737
|
+
### Storage Adapters
|
|
9738
|
+
- \`ChromeStorageAdapter\` - Uses \`chrome.storage.local\` (auto-detected in extensions)
|
|
9739
|
+
- \`IndexedDBAdapter\` - Uses IndexedDB (auto-detected in web apps)
|
|
9740
|
+
- \`MemoryStorageAdapter\` - Uses in-memory Map (auto-detected in Node.js)
|
|
9741
|
+
|
|
9742
|
+
### Sync Adapters
|
|
9743
|
+
- \`ChromeRuntimeSyncAdapter\` - Uses \`chrome.runtime.sendMessage\` (auto-detected in extensions)
|
|
9744
|
+
- \`BroadcastChannelSyncAdapter\` - Uses BroadcastChannel API (auto-detected in web apps)
|
|
9745
|
+
- \`NoOpSyncAdapter\` - No synchronization (auto-detected for single-context scenarios)
|
|
9746
|
+
|
|
9747
|
+
## Custom Adapters
|
|
9748
|
+
|
|
9749
|
+
Users can provide custom adapters for specialized scenarios:
|
|
9750
|
+
|
|
9751
|
+
\`\`\`typescript
|
|
9752
|
+
import { $sharedState } from '@fairfox/polly/state';
|
|
9753
|
+
import { IndexedDBAdapter, BroadcastChannelSyncAdapter } from '@fairfox/polly/adapters';
|
|
9754
|
+
|
|
9755
|
+
const settings = $sharedState('settings', defaultSettings, {
|
|
9756
|
+
storage: new IndexedDBAdapter('custom-db-name'),
|
|
9757
|
+
sync: new BroadcastChannelSyncAdapter('custom-channel'),
|
|
9758
|
+
});
|
|
9759
|
+
\`\`\`
|
|
9760
|
+
|
|
9761
|
+
## Key Features
|
|
9762
|
+
|
|
9763
|
+
- **Write once, run anywhere**: Same state code works in extensions, web apps, and Node.js
|
|
9764
|
+
- **Environment-optimized**: Uses the best available APIs for each platform
|
|
9765
|
+
- **No conditional logic**: Framework handles detection and selection
|
|
9766
|
+
- **Testable**: Mock adapters enable testing without Chrome APIs
|
|
9767
|
+
- **Extensible**: Custom adapters for specialized use cases
|
|
9768
|
+
|
|
9769
|
+
## Common Use Cases
|
|
9770
|
+
|
|
9771
|
+
### Multi-Tab Web Application
|
|
9772
|
+
\`\`\`typescript
|
|
9773
|
+
// State automatically syncs across tabs using BroadcastChannel
|
|
9774
|
+
const todos = $sharedState('todos', []);
|
|
9775
|
+
\`\`\`
|
|
9776
|
+
|
|
9777
|
+
### Chrome Extension
|
|
9778
|
+
\`\`\`typescript
|
|
9779
|
+
// State syncs across extension contexts using chrome.runtime
|
|
9780
|
+
const settings = $sharedState('settings', { theme: 'dark' });
|
|
9781
|
+
\`\`\`
|
|
9782
|
+
|
|
9783
|
+
### Single-Tab Web App (PWA)
|
|
9784
|
+
\`\`\`typescript
|
|
9785
|
+
// State persists to IndexedDB but doesn't sync (no other tabs)
|
|
9786
|
+
const user = $sharedState('user', null, {
|
|
9787
|
+
sync: new NoOpSyncAdapter() // Explicitly disable sync
|
|
9788
|
+
});
|
|
9789
|
+
\`\`\`
|
|
9790
|
+
|
|
9791
|
+
### Node.js Testing
|
|
9792
|
+
\`\`\`typescript
|
|
9793
|
+
// State uses in-memory storage, no persistence or sync
|
|
9794
|
+
const mockState = $sharedState('test-state', defaultValue);
|
|
9795
|
+
\`\`\`
|
|
9796
|
+
|
|
9797
|
+
## Migration from Chrome-Only
|
|
9798
|
+
|
|
9799
|
+
Existing Chrome extension code requires **no changes** - the framework is backward compatible. Web apps can now use the same primitives that previously only worked in extensions.
|
|
9800
|
+
|
|
9385
9801
|
Begin by understanding their question and providing a clear, precise answer based on their project context.`;
|
|
9386
9802
|
}
|
|
9387
9803
|
function generateProjectSection(context, contexts2, handlers2, flows2) {
|
|
@@ -9531,6 +9947,11 @@ confidence that they will work when users apply them to their configuration.
|
|
|
9531
9947
|
Constraints and type guards can be organized in separate files (e.g., specs/constraints.ts) and will
|
|
9532
9948
|
be automatically discovered via imports. Files outside src/ are fully supported.
|
|
9533
9949
|
|
|
9950
|
+
**Verified State Discovery**: When \`{ verify: true }\` is set on $sharedState/$syncedState/$persistedState,
|
|
9951
|
+
the analyzer automatically discovers exported functions that modify that state and extracts their
|
|
9952
|
+
\`requires()\`/\`ensures()\` annotations. This enables verification for apps that don't use \`messageBus.on()\`
|
|
9953
|
+
handlers, such as multi-tab PWAs or WebSocket applications with reactive effect-driven state changes.
|
|
9954
|
+
|
|
9534
9955
|
# Communication Style
|
|
9535
9956
|
|
|
9536
9957
|
- Direct and precise - no fluff
|
|
@@ -9544,6 +9965,32 @@ ${generateProjectSection(context, contexts2, allHandlers, messageFlows)}
|
|
|
9544
9965
|
|
|
9545
9966
|
${generateVerificationSection(context)}
|
|
9546
9967
|
|
|
9968
|
+
# Understanding Verification Parameters
|
|
9969
|
+
|
|
9970
|
+
Before suggesting optimizations, understand what each parameter actually controls:
|
|
9971
|
+
|
|
9972
|
+
**maxDepth**: Maximum number of **sequential** message deliveries to check
|
|
9973
|
+
- Depth 4 = check sequences up to 4 messages delivered one after another
|
|
9974
|
+
- Depth 8 = check sequences up to 8 messages delivered one after another
|
|
9975
|
+
- Question to ask: "What's the longest sequence of messages that could expose a bug in my app?"
|
|
9976
|
+
- Most authentication/state machine bugs appear in shallow sequences (depth 2-4)
|
|
9977
|
+
|
|
9978
|
+
**maxInFlight**: Maximum number of **concurrent** messages in the system at once
|
|
9979
|
+
- 2 = check scenarios with up to 2 messages pending simultaneously
|
|
9980
|
+
- 3 = check scenarios with up to 3 messages pending simultaneously
|
|
9981
|
+
- Question: "How many concurrent messages can actually happen in my app?"
|
|
9982
|
+
- Consider different bounds per message type (auth should be 1, queries could be higher)
|
|
9983
|
+
|
|
9984
|
+
**maxTabs**: Number of concurrent contexts (tabs, workers, etc.)
|
|
9985
|
+
- Question: "How many concurrent contexts do I need to verify for race conditions?"
|
|
9986
|
+
- Most single-tab apps only need 1; multi-tab/worker apps need 2+
|
|
9987
|
+
|
|
9988
|
+
**Practical Approach**:
|
|
9989
|
+
- Start conservative: maxDepth: 3-4, maxInFlight: 2, maxTabs: 1
|
|
9990
|
+
- Use timeouts (5-10 minutes) to prevent runaway verification
|
|
9991
|
+
- Don't arbitrarily exclude message types - tune the bounds instead
|
|
9992
|
+
- If verification takes hours, bounds are likely too high for regular development
|
|
9993
|
+
|
|
9547
9994
|
# Your Expertise: Optimization Tiers
|
|
9548
9995
|
|
|
9549
9996
|
## Tier 1: Safe Optimizations (ZERO precision loss)
|
|
@@ -9847,4 +10294,4 @@ Goodbye!`);
|
|
|
9847
10294
|
}
|
|
9848
10295
|
main();
|
|
9849
10296
|
|
|
9850
|
-
//# debugId=
|
|
10297
|
+
//# debugId=4FDE5BCEB24CA52B64756E2164756E21
|