@fairfox/polly 0.13.2 → 0.14.1

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.
@@ -92,13 +92,24 @@ export declare class HandlerExtractor {
92
92
  */
93
93
  private extractSimpleOrElementAccessAssignment;
94
94
  /**
95
- * Extract property access assignment (state.field = value)
95
+ * Extract property access assignment (state.field = value or someState.value.field = value)
96
96
  */
97
97
  private extractPropertyAccessAssignment;
98
+ /**
99
+ * Extract assignments from object literal properties
100
+ * Used for: someState.value = { field1: value1, field2: value2 }
101
+ */
102
+ private extractObjectLiteralAssignments;
98
103
  /**
99
104
  * Extract element access assignment (state.items[index] = value)
100
105
  */
101
106
  private extractElementAccessAssignment;
107
+ /**
108
+ * Check if field path is a state mutation pattern and extract field name
109
+ * Supports: state.field, someState.value.field
110
+ * Returns: field name or null if not a state pattern
111
+ */
112
+ private extractFieldFromStatePath;
102
113
  /**
103
114
  * Extract compound assignments (+=, -=, *=, /=, %=)
104
115
  */
@@ -5747,6 +5747,39 @@ class HandlerExtractor {
5747
5747
  if (value !== undefined) {
5748
5748
  assignments.push({ field, value });
5749
5749
  }
5750
+ return;
5751
+ }
5752
+ const valueMatch = fieldPath.match(/\.value\.(.+)$/);
5753
+ if (valueMatch && valueMatch[1]) {
5754
+ const field = valueMatch[1];
5755
+ const value = this.extractValue(right);
5756
+ if (value !== undefined) {
5757
+ assignments.push({ field, value });
5758
+ }
5759
+ return;
5760
+ }
5761
+ if (fieldPath.endsWith(".value") && Node4.isObjectLiteralExpression(right)) {
5762
+ this.extractObjectLiteralAssignments(right, assignments);
5763
+ }
5764
+ }
5765
+ extractObjectLiteralAssignments(objectLiteral, assignments) {
5766
+ if (!Node4.isObjectLiteralExpression(objectLiteral))
5767
+ return;
5768
+ for (const prop of objectLiteral.getProperties()) {
5769
+ if (Node4.isPropertyAssignment(prop)) {
5770
+ const name = prop.getName();
5771
+ const initializer = prop.getInitializer();
5772
+ if (!name || !initializer)
5773
+ continue;
5774
+ const value = this.extractValue(initializer);
5775
+ if (value !== undefined) {
5776
+ assignments.push({ field: name, value });
5777
+ }
5778
+ }
5779
+ if (Node4.isShorthandPropertyAssignment(prop)) {
5780
+ const name = prop.getName();
5781
+ assignments.push({ field: name, value: "@" });
5782
+ }
5750
5783
  }
5751
5784
  }
5752
5785
  extractElementAccessAssignment(left, right, assignments) {
@@ -5756,9 +5789,9 @@ class HandlerExtractor {
5756
5789
  if (!Node4.isPropertyAccessExpression(expr))
5757
5790
  return;
5758
5791
  const fieldPath = this.getPropertyPath(expr);
5759
- if (!fieldPath.startsWith("state."))
5792
+ const field = this.extractFieldFromStatePath(fieldPath);
5793
+ if (!field)
5760
5794
  return;
5761
- const field = fieldPath.substring(6);
5762
5795
  const indexExpr = left.getArgumentExpression();
5763
5796
  const index = indexExpr ? indexExpr.getText() : "0";
5764
5797
  const value = this.extractValue(right);
@@ -5770,6 +5803,16 @@ class HandlerExtractor {
5770
5803
  });
5771
5804
  }
5772
5805
  }
5806
+ extractFieldFromStatePath(fieldPath) {
5807
+ if (fieldPath.startsWith("state.")) {
5808
+ return fieldPath.substring(6);
5809
+ }
5810
+ const valueMatch = fieldPath.match(/\.value\.(.+)$/);
5811
+ if (valueMatch && valueMatch[1]) {
5812
+ return valueMatch[1];
5813
+ }
5814
+ return null;
5815
+ }
5773
5816
  extractCompoundAssignment(node, assignments) {
5774
5817
  if (!Node4.isBinaryExpression(node))
5775
5818
  return;
@@ -5778,8 +5821,8 @@ class HandlerExtractor {
5778
5821
  const right = node.getRight();
5779
5822
  if (Node4.isPropertyAccessExpression(left)) {
5780
5823
  const fieldPath = this.getPropertyPath(left);
5781
- if (fieldPath.startsWith("state.")) {
5782
- const field = fieldPath.substring(6);
5824
+ const field = this.extractFieldFromStatePath(fieldPath);
5825
+ if (field) {
5783
5826
  const rightValue = right.getText();
5784
5827
  const tlaOp = operator.slice(0, -1);
5785
5828
  assignments.push({
@@ -5813,8 +5856,8 @@ class HandlerExtractor {
5813
5856
  if (!Node4.isPropertyAccessExpression(operand))
5814
5857
  return;
5815
5858
  const fieldPath = this.getPropertyPath(operand);
5816
- if (fieldPath.startsWith("state.")) {
5817
- const field = fieldPath.substring(6);
5859
+ const field = this.extractFieldFromStatePath(fieldPath);
5860
+ if (field) {
5818
5861
  const value = operatorText === "++" ? "@ + 1" : "@ - 1";
5819
5862
  assignments.push({ field, value });
5820
5863
  }
@@ -5829,8 +5872,8 @@ class HandlerExtractor {
5829
5872
  const object = expr.getExpression();
5830
5873
  if (Node4.isPropertyAccessExpression(object)) {
5831
5874
  const fieldPath = this.getPropertyPath(object);
5832
- if (fieldPath.startsWith("state.")) {
5833
- const field = fieldPath.substring(6);
5875
+ const field = this.extractFieldFromStatePath(fieldPath);
5876
+ if (field) {
5834
5877
  const args = node.getArguments().map((arg) => arg.getText());
5835
5878
  const tlaValue = this.translateArrayMethod(methodName, args, fieldPath);
5836
5879
  if (tlaValue) {
@@ -6470,8 +6513,8 @@ class HandlerExtractor {
6470
6513
  if (!Node4.isPropertyAccessExpression(left) && !Node4.isElementAccessExpression(left))
6471
6514
  return;
6472
6515
  const fieldPath = Node4.isPropertyAccessExpression(left) ? this.getPropertyPath(left) : this.getPropertyPath(left.getExpression());
6473
- if (fieldPath.startsWith("state.")) {
6474
- const field = fieldPath.substring(6);
6516
+ const field = this.extractFieldFromStatePath(fieldPath);
6517
+ if (field) {
6475
6518
  const line = node.getStartLineNumber();
6476
6519
  const afterAwait = node.getStart() > firstAwaitPos;
6477
6520
  mutations.push({ field, line, afterAwait });
@@ -6488,10 +6531,8 @@ class HandlerExtractor {
6488
6531
  if (!Node4.isPropertyAccessExpression(object))
6489
6532
  return;
6490
6533
  const fieldPath = this.getPropertyPath(object);
6491
- if (!fieldPath.startsWith("state."))
6492
- return;
6493
- if (["push", "pop", "shift", "unshift", "splice"].includes(methodName)) {
6494
- const field = fieldPath.substring(6);
6534
+ const field = this.extractFieldFromStatePath(fieldPath);
6535
+ if (field && ["push", "pop", "shift", "unshift", "splice"].includes(methodName)) {
6495
6536
  const line = node.getStartLineNumber();
6496
6537
  const afterAwait = node.getStart() > firstAwaitPos;
6497
6538
  mutations.push({ field, line, afterAwait });
@@ -9061,6 +9102,32 @@ ${generateVerificationSection(context)}
9061
9102
  - Files outside src/ are automatically found if imported from handler files
9062
9103
  - This enables clean separation of verification code from runtime code
9063
9104
 
9105
+ # Verification Parameters Explained
9106
+
9107
+ When explaining verification configuration, help users understand what each parameter controls:
9108
+
9109
+ **maxDepth**: Maximum number of **sequential** message deliveries to check
9110
+ - Depth 4 = check sequences up to 4 messages delivered one after another
9111
+ - Depth 8 = check sequences up to 8 messages delivered one after another
9112
+ - Question to ask: "What's the longest sequence of messages that could expose a bug in my app?"
9113
+ - Most authentication/state machine bugs appear in shallow sequences (depth 2-4)
9114
+
9115
+ **maxInFlight**: Maximum number of **concurrent** messages in the system at once
9116
+ - 2 = check scenarios with up to 2 messages pending simultaneously
9117
+ - 3 = check scenarios with up to 3 messages pending simultaneously
9118
+ - Question: "How many concurrent messages can actually happen in my app?"
9119
+ - Consider different bounds per message type (auth should be 1, queries could be higher)
9120
+
9121
+ **maxTabs**: Number of concurrent contexts (tabs, workers, etc.)
9122
+ - Question: "How many concurrent contexts do I need to verify for race conditions?"
9123
+ - Most single-tab apps only need 1; multi-tab/worker apps need 2+
9124
+
9125
+ **Practical Guidance**:
9126
+ - Start conservative: maxDepth: 3-4, maxInFlight: 2, maxTabs: 1
9127
+ - Use timeouts (5-10 minutes) to prevent runaway verification
9128
+ - Don't arbitrarily exclude message types to speed up verification - tune the bounds instead
9129
+ - If verification takes hours, bounds are likely too high for regular development workflows
9130
+
9064
9131
  # Elysia/Bun Integration
9065
9132
 
9066
9133
  Polly now provides first-class support for Elysia (Bun-first web framework) with Eden type-safe client generation:
@@ -9109,6 +9176,8 @@ const app = new Elysia()
9109
9176
  'POST /todos': {
9110
9177
  queue: true,
9111
9178
  optimistic: (body) => ({ id: -Date.now(), ...body }),
9179
+ merge: 'replace', // 'replace' | custom merge function
9180
+ conflictResolution: 'last-write-wins', // 'last-write-wins' | 'server-wins' | custom function
9112
9181
  },
9113
9182
  },
9114
9183
  }))
@@ -9125,6 +9194,30 @@ const app = new Elysia()
9125
9194
  - In production: Pass-through (minimal overhead) - client effects are bundled at build time
9126
9195
  - Authorization and broadcasts work in both modes
9127
9196
 
9197
+ **Offline Behavior Configuration:**
9198
+
9199
+ The \`offline\` config enables Progressive Web App capabilities:
9200
+
9201
+ \`\`\`typescript
9202
+ offline: {
9203
+ 'POST /todos': {
9204
+ queue: true, // Queue request when offline
9205
+ optimistic: (body) => TResult, // Immediate UI update with predicted result
9206
+ merge: 'replace' | mergeFn, // How to merge optimistic with server result
9207
+ conflictResolution: 'last-write-wins' | 'server-wins' | resolveFn,
9208
+ },
9209
+ }
9210
+ \`\`\`
9211
+
9212
+ **Merge Strategies:**
9213
+ - \`'replace'\`: Replace optimistic result with server result (default)
9214
+ - \`(optimistic, server) => TResult\`: Custom merge logic
9215
+
9216
+ **Conflict Resolution** (when multiple devices edit offline):
9217
+ - \`'last-write-wins'\`: Lamport clock determines winner
9218
+ - \`'server-wins'\`: Server state always takes precedence
9219
+ - \`(client, server) => TResult\`: Custom conflict resolution
9220
+
9128
9221
  ## Client-Side Wrapper (\`@fairfox/polly/client\`)
9129
9222
 
9130
9223
  Enhances Eden treaty client with Polly features:
@@ -9503,6 +9596,32 @@ ${generateProjectSection(context, contexts2, allHandlers, messageFlows)}
9503
9596
 
9504
9597
  ${generateVerificationSection(context)}
9505
9598
 
9599
+ # Understanding Verification Parameters
9600
+
9601
+ Before suggesting optimizations, understand what each parameter actually controls:
9602
+
9603
+ **maxDepth**: Maximum number of **sequential** message deliveries to check
9604
+ - Depth 4 = check sequences up to 4 messages delivered one after another
9605
+ - Depth 8 = check sequences up to 8 messages delivered one after another
9606
+ - Question to ask: "What's the longest sequence of messages that could expose a bug in my app?"
9607
+ - Most authentication/state machine bugs appear in shallow sequences (depth 2-4)
9608
+
9609
+ **maxInFlight**: Maximum number of **concurrent** messages in the system at once
9610
+ - 2 = check scenarios with up to 2 messages pending simultaneously
9611
+ - 3 = check scenarios with up to 3 messages pending simultaneously
9612
+ - Question: "How many concurrent messages can actually happen in my app?"
9613
+ - Consider different bounds per message type (auth should be 1, queries could be higher)
9614
+
9615
+ **maxTabs**: Number of concurrent contexts (tabs, workers, etc.)
9616
+ - Question: "How many concurrent contexts do I need to verify for race conditions?"
9617
+ - Most single-tab apps only need 1; multi-tab/worker apps need 2+
9618
+
9619
+ **Practical Approach**:
9620
+ - Start conservative: maxDepth: 3-4, maxInFlight: 2, maxTabs: 1
9621
+ - Use timeouts (5-10 minutes) to prevent runaway verification
9622
+ - Don't arbitrarily exclude message types - tune the bounds instead
9623
+ - If verification takes hours, bounds are likely too high for regular development
9624
+
9506
9625
  # Your Expertise: Optimization Tiers
9507
9626
 
9508
9627
  ## Tier 1: Safe Optimizations (ZERO precision loss)
@@ -9806,4 +9925,4 @@ Goodbye!`);
9806
9925
  }
9807
9926
  main();
9808
9927
 
9809
- //# debugId=53EFCFE442D6558B64756E2164756E21
9928
+ //# debugId=0C5B6EFE146F7BE464756E2164756E21