@fairfox/polly 0.14.1 → 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 +376 -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 +232 -7
- package/dist/tools/verify/src/cli.js.map +3 -3
- 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,23 @@ ${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
|
+
\`\`\`
|
|
9104
9348
|
|
|
9105
9349
|
# Verification Parameters Explained
|
|
9106
9350
|
|
|
@@ -9434,6 +9678,126 @@ All context creation functions now follow the adapter pattern:
|
|
|
9434
9678
|
- \`getMessageBus(context, adapters?)\` ✓
|
|
9435
9679
|
- \`new MessageBus(context, adapters?)\` ✓
|
|
9436
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
|
+
|
|
9437
9801
|
Begin by understanding their question and providing a clear, precise answer based on their project context.`;
|
|
9438
9802
|
}
|
|
9439
9803
|
function generateProjectSection(context, contexts2, handlers2, flows2) {
|
|
@@ -9583,6 +9947,11 @@ confidence that they will work when users apply them to their configuration.
|
|
|
9583
9947
|
Constraints and type guards can be organized in separate files (e.g., specs/constraints.ts) and will
|
|
9584
9948
|
be automatically discovered via imports. Files outside src/ are fully supported.
|
|
9585
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
|
+
|
|
9586
9955
|
# Communication Style
|
|
9587
9956
|
|
|
9588
9957
|
- Direct and precise - no fluff
|
|
@@ -9925,4 +10294,4 @@ Goodbye!`);
|
|
|
9925
10294
|
}
|
|
9926
10295
|
main();
|
|
9927
10296
|
|
|
9928
|
-
//# debugId=
|
|
10297
|
+
//# debugId=4FDE5BCEB24CA52B64756E2164756E21
|