@novasamatech/statement-store 0.8.7-3 → 0.8.7-5

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.
@@ -97,9 +97,9 @@ export function createPapiStatementStoreAdapter(lazyClient) {
97
97
  case 'dataTooLarge':
98
98
  return errAsync(new DataTooLargeError(result.submitted_size, result.available_size));
99
99
  case 'channelPriorityTooLow':
100
- return errAsync(new ExpiryTooLowError(result.submitted_expiry, result.min_expiry));
100
+ return errAsync(new ExpiryTooLowError(BigInt(result.submitted_expiry), BigInt(result.min_expiry)));
101
101
  case 'accountFull':
102
- return errAsync(new AccountFullError(result.submitted_expiry, result.min_expiry));
102
+ return errAsync(new AccountFullError(BigInt(result.submitted_expiry), BigInt(result.min_expiry)));
103
103
  case 'storeFull':
104
104
  return errAsync(new StorageFullError());
105
105
  case 'noAllowance':
@@ -99,12 +99,19 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
99
99
  // latest is live — a retry for an older one must not resurrect it).
100
100
  let lastResponseRequestId = null;
101
101
  // Encrypt, then submit on `channel`/`topicSessionId` at the allocator's next (strictly
102
- // increasing) expiry. On a priority rejection submitStatementOnce resyncs the allocator to the
103
- // chain-reported minimum before propagating, so the retry and every later submit — clears it.
102
+ // increasing) expiry. A priority rejection does NOT resync the allocator here — each caller
103
+ // raises the floor to the chain-reported minimum (the submitWithRetry `onPriorityError` hooks
104
+ // below, and the one-shot clear in clearOutgoingStatement), so the retry — and every later
105
+ // submit — clears it.
104
106
  function submitStatementData(channel, topicSessionId, data) {
105
- return encryption
106
- .encrypt(data)
107
- .asyncAndThen(encrypted => submitStatementOnce({ statementStore, prover, allocator, channel, topics: [topicSessionId], data: encrypted }));
107
+ return encryption.encrypt(data).asyncAndThen(encrypted => submitStatementOnce({
108
+ statementStore,
109
+ prover,
110
+ allocator,
111
+ channel,
112
+ topics: [topicSessionId],
113
+ data: encrypted,
114
+ }));
108
115
  }
109
116
  // Settle and remove the pending-delivery entries for the given tokens.
110
117
  function settleTokens(tokens, settle) {
@@ -118,8 +125,8 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
118
125
  }
119
126
  // Session retry policy (this and every submitWithRetry call below): priority errors
120
127
  // (ExpiryTooLow / AccountFull) are retried with `priorityAttempts: 'unbounded'` — they never
121
- // consume the transient-failure budget, because submitStatementData has already resynced the
122
- // allocator above the chain-reported minimum, so the next attempt submits higher. We keep at it
128
+ // consume the transient-failure budget, because the `onPriorityError` hook raises the allocator
129
+ // floor above the chain-reported minimum, so the next attempt submits higher. We keep at it
123
130
  // until the statement lands or the submission is superseded; once superseded, a priority
124
131
  // rejection is swallowed as success (it merely lost the channel race to a newer, higher-priority
125
132
  // statement). The upshot: priority errors never surface to session callers. Other errors keep
@@ -132,6 +139,8 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
132
139
  attempts: MAX_SUBMIT_RETRIES,
133
140
  priorityAttempts: 'unbounded',
134
141
  delaysMs: RETRY_DELAY_MS,
142
+ // Adopt the chain-reported floor so the next attempt submits strictly above it.
143
+ onPriorityError: error => allocator.raiseFloor(error.min),
135
144
  // Only keep retrying while this is still the live submission (not superseded by a
136
145
  // newer retransmit, aborted via clearOutgoingStatement, or disposed).
137
146
  shouldRetry: () => !disposed && state.outgoingRequest?.requestIds.at(-1) === requestId,
@@ -454,6 +463,8 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
454
463
  attempts: MAX_SUBMIT_RETRIES,
455
464
  priorityAttempts: 'unbounded',
456
465
  delaysMs: RETRY_DELAY_MS,
466
+ // Adopt the chain-reported floor so the next attempt submits strictly above it.
467
+ onPriorityError: error => allocator.raiseFloor(error.min),
457
468
  // Stop retrying once a newer response supersedes this one (shared response channel) or disposed.
458
469
  shouldRetry: () => !disposed && lastResponseRequestId === requestId,
459
470
  })
@@ -576,8 +587,14 @@ export function createSession({ localAccount, remoteAccount, statementStore, enc
576
587
  // would leave the original request live on-chain. One shot, no retry (clearing is a
577
588
  // supersede, not a request that must land); a priority rejection (ExpiryTooLow /
578
589
  // AccountFull) means the channel already advanced past us, so the clear already
579
- // happened → absorb it as success.
580
- return submitStatementData(requestChannel, outgoingSessionId, encoded.value).orElse(error => isPriorityTooLow(error) ? okAsync(undefined) : errAsync(error));
590
+ // happened → absorb it as success. No retry loop here means no onPriorityError hook, so
591
+ // resync the allocator inline: adopt the chain floor before absorbing, so later submits stay above it.
592
+ return submitStatementData(requestChannel, outgoingSessionId, encoded.value).orElse(error => {
593
+ if (!isPriorityTooLow(error))
594
+ return errAsync(error);
595
+ allocator.raiseFloor(error.min);
596
+ return okAsync(undefined);
597
+ });
581
598
  },
582
599
  dispose() {
583
600
  disposed = true;
@@ -28,5 +28,13 @@ export type SubmitRetryOptions = {
28
28
  delayMs: number;
29
29
  error: Error;
30
30
  }) => void;
31
+ /**
32
+ * Invoked on every priority rejection (ExpiryTooLow / AccountFull), before the retry
33
+ * decision — so it fires even when the priority budget is exhausted or the submission is
34
+ * no longer live. This is where the caller adopts the chain-reported floor (`error.min`)
35
+ * into its allocator, so the NEXT attempt — here or via an outer retry/outbox — submits
36
+ * strictly above it and clears in one step rather than climbing.
37
+ */
38
+ onPriorityError?: (error: ExpiryTooLowError | AccountFullError) => void;
31
39
  };
32
40
  export declare function submitWithRetry(submit: () => ResultAsync<void, Error>, options: SubmitRetryOptions): ResultAsync<void, Error>;
@@ -15,7 +15,7 @@ function delayFor(delaysMs, attempt) {
15
15
  return delaysMs[Math.min(attempt, delaysMs.length - 1)] ?? 0;
16
16
  }
17
17
  export function submitWithRetry(submit, options) {
18
- const { attempts, priorityAttempts, delaysMs, shouldRetry = () => true, onRetry } = options;
18
+ const { attempts, priorityAttempts, delaysMs, shouldRetry = () => true, onRetry, onPriorityError } = options;
19
19
  // How to settle once we stop retrying: under the 'unbounded' policy a
20
20
  // no-longer-live submission rejected with a priority error simply lost the
21
21
  // channel race to a newer, higher-priority statement — benign, so report success.
@@ -30,13 +30,17 @@ export function submitWithRetry(submit, options) {
30
30
  if (result.isOk())
31
31
  return result;
32
32
  const error = result.error;
33
- const priority = isPriorityTooLow(error);
34
- const budgetLeft = priority ? priorityLeft : attemptsLeft;
33
+ const priorityError = isPriorityTooLow(error) ? error : null;
34
+ // Adopt the chain-reported floor before deciding whether to retry, so an exhausted or
35
+ // no-longer-live priority rejection still raises it for any outer retry/outbox.
36
+ if (priorityError)
37
+ onPriorityError?.(priorityError);
38
+ const budgetLeft = priorityError ? priorityLeft : attemptsLeft;
35
39
  if (!shouldRetry() || (typeof budgetLeft === 'number' && budgetLeft <= 0))
36
40
  return settle(error);
37
41
  const delayMs = delayFor(delaysMs, attempt);
38
42
  onRetry?.({ attempt, delayMs, error });
39
- if (priority) {
43
+ if (priorityError) {
40
44
  if (priorityLeft !== 'unbounded')
41
45
  priorityLeft -= 1;
42
46
  }
@@ -36,6 +36,26 @@ describe('submitWithRetry', () => {
36
36
  expect(result._unsafeUnwrapErr()).toBeInstanceOf(AccountFullError);
37
37
  expect(submit).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
38
38
  });
39
+ it('onPriorityError fires for every priority rejection, including the terminal one once the budget is exhausted', async () => {
40
+ const seen = [];
41
+ let calls = 0;
42
+ const submit = vi.fn(() => errAsync(new AccountFullError(0n, BigInt(++calls))));
43
+ const result = await submitWithRetry(submit, {
44
+ ...FAST,
45
+ attempts: 0,
46
+ priorityAttempts: 2,
47
+ onPriorityError: error => seen.push(error.min),
48
+ });
49
+ expect(result.isErr()).toBe(true);
50
+ expect(submit).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
51
+ expect(seen).toEqual([1n, 2n, 3n]); // adopted the floor on all three, including the terminal rejection
52
+ });
53
+ it('onPriorityError is not called for non-priority errors', async () => {
54
+ const onPriorityError = vi.fn();
55
+ const submit = vi.fn(() => errAsync(new Error('store rejected')));
56
+ await submitWithRetry(submit, { ...FAST, attempts: 2, priorityAttempts: 'unbounded', onPriorityError });
57
+ expect(onPriorityError).not.toHaveBeenCalled();
58
+ });
39
59
  it('attempts 0: a non-priority error propagates immediately', async () => {
40
60
  const submit = vi.fn(() => errAsync(new Error('store rejected')));
41
61
  const result = await submitWithRetry(submit, { ...FAST, attempts: 0, priorityAttempts: 3 });
@@ -24,5 +24,9 @@ export function submitStatementOnce(params) {
24
24
  });
25
25
  }
26
26
  export function signAndSubmitStatement(params) {
27
- return submitWithRetry(() => submitStatementOnce(params), params.retry);
27
+ return submitWithRetry(() => submitStatementOnce(params), {
28
+ ...params.retry,
29
+ // Adopt the chain-reported floor so the next retry submits strictly above it and clears.
30
+ onPriorityError: error => params.allocator.raiseFloor(error.min),
31
+ });
28
32
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/statement-store",
3
3
  "type": "module",
4
- "version": "0.8.7-3",
4
+ "version": "0.8.7-5",
5
5
  "description": "Statement store integration",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -25,9 +25,9 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@novasamatech/scale": "0.8.7-3",
28
+ "@novasamatech/scale": "0.8.7-5",
29
29
  "@novasamatech/sdk-statement": "^0.6.0",
30
- "@novasamatech/substrate-slot-sr25519-wasm": "0.8.7-3",
30
+ "@novasamatech/substrate-slot-sr25519-wasm": "0.8.7-5",
31
31
  "@polkadot-api/substrate-bindings": "^0.20.3",
32
32
  "@polkadot-api/substrate-client": "^0.7.0",
33
33
  "@polkadot-labs/hdkd-helpers": "^0.0.30",