@develit-services/bank 5.9.1 → 6.0.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/base.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const backendSdk = require('@develit-io/backend-sdk');
4
4
  const paymentDirection = require('./shared/bank.BsWBG0gb.cjs');
5
- const bank = require('./shared/bank.DBTtRCcf.cjs');
5
+ const bank = require('./shared/bank.DQZxSHKV.cjs');
6
6
  const drizzleOrm = require('drizzle-orm');
7
7
  const cloudflare_workers = require('cloudflare:workers');
8
8
  const d1 = require('drizzle-orm/d1');
@@ -107,67 +107,31 @@ async function heartbeatSyncWorkflows({
107
107
  );
108
108
  }
109
109
 
110
- const DISPATCH_RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
111
- function buildDispatchInstanceId(accountId, nowMs, recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS) {
112
- return `${accountId}-${Math.floor(nowMs / recoveryWindowMs)}`;
113
- }
114
- function buildDispatchCreateOptions(instanceId, accountId, maxIterations) {
115
- return {
116
- id: instanceId,
117
- // Omitted (not undefined) when unresolved: the workflow treats a missing
118
- // budget as unbounded — degraded but syncing.
119
- params: maxIterations == null ? { accountId } : { accountId, maxIterations },
120
- // Dispatch mints ~24 instances per account per day and step outputs carry
121
- // bank payloads — the default 30-day retention would pile up billable
122
- // storage for state nobody reads after success.
123
- retention: {
124
- successRetention: "1 day",
125
- errorRetention: "7 days"
126
- }
127
- };
128
- }
129
- function isAlreadyExists(err) {
130
- const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : "";
131
- return /already exists/i.test(message);
132
- }
133
-
134
- const DEFAULT_MIN_GRACE_MS = 3 * 60 * 1e3;
135
110
  async function dispatchSyncWorkflows({
136
111
  entities,
137
112
  createInstance,
138
113
  logger,
139
114
  now = Date.now,
140
- minGraceMs = DEFAULT_MIN_GRACE_MS,
141
- recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS
115
+ recoveryWindowMs = bank.DISPATCH_RECOVERY_WINDOW_MS
142
116
  }) {
143
117
  const at = now();
144
118
  await Promise.all(
145
119
  entities.map(async (entity) => {
146
120
  const { id, lastSyncAt } = entity;
147
- const staleness = assessSyncStaleness(entity, at, minGraceMs);
148
- if (staleness == null) {
121
+ if (!lastSyncAt) {
149
122
  logger.warn("sync-workflow.dispatch.sync-age-unknown", { id });
150
123
  return;
151
124
  }
152
- if (staleness.staleForMs <= staleness.thresholdMs) return;
153
- const instanceId = buildDispatchInstanceId(id, at, recoveryWindowMs);
125
+ const instanceId = bank.buildDispatchInstanceId(id, at, recoveryWindowMs);
154
126
  try {
155
127
  await createInstance(instanceId, id);
156
128
  logger.info("sync-workflow.dispatch.created", {
157
129
  id,
158
130
  instanceId,
159
- lastSyncAt: lastSyncAt?.toISOString() ?? null,
160
- staleForS: Math.round(staleness.staleForMs / 1e3),
161
- thresholdS: Math.round(staleness.thresholdMs / 1e3)
131
+ lastSyncAt: lastSyncAt.toISOString()
162
132
  });
163
133
  } catch (err) {
164
- if (isAlreadyExists(err)) {
165
- logger.info("sync-workflow.dispatch.already-running", {
166
- id,
167
- instanceId
168
- });
169
- return;
170
- }
134
+ if (bank.isAlreadyExists(err)) return;
171
135
  logger.error("sync-workflow.dispatch.failed", {
172
136
  id,
173
137
  instanceId,
@@ -1028,17 +992,14 @@ exports.BankServiceBase = class BankServiceBase extends backendSdk.develitWorker
1028
992
  this.env.SYNC_DISPATCH_ACCOUNT_IDS,
1029
993
  this.env.CRON_SYNC_WORKFLOW_DISPATCH
1030
994
  )) {
1031
- const instanceId = buildDispatchInstanceId(accountId, Date.now());
1032
- const maxIterations = bank.parseIterationBudgetParam(
1033
- this.env.SYNC_WORKFLOW_MAX_ITERATIONS
1034
- );
995
+ const instanceId = bank.buildDispatchInstanceId(accountId, Date.now());
1035
996
  let instance2;
1036
997
  try {
1037
998
  instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
1038
- buildDispatchCreateOptions(instanceId, accountId, maxIterations)
999
+ bank.buildDispatchCreateOptions(instanceId, accountId)
1039
1000
  );
1040
1001
  } catch (err) {
1041
- if (!isAlreadyExists(err)) throw err;
1002
+ if (!bank.isAlreadyExists(err)) throw err;
1042
1003
  instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(instanceId);
1043
1004
  }
1044
1005
  return {
@@ -1078,8 +1039,41 @@ exports.BankServiceBase = class BankServiceBase extends backendSdk.develitWorker
1078
1039
  { successMessage: "Account sync workflow restarted" },
1079
1040
  async ({ accountId }) => {
1080
1041
  await this.setSyncEnabledOrThrow(accountId, true);
1081
- const instance = await this.getCurrentSyncInstance(accountId);
1082
- await instance.restart();
1042
+ if (isDispatchActive(
1043
+ accountId,
1044
+ this.env.SYNC_DISPATCH_ACCOUNT_IDS,
1045
+ this.env.CRON_SYNC_WORKFLOW_DISPATCH
1046
+ )) {
1047
+ const instanceId = bank.buildDispatchInstanceId(accountId, Date.now());
1048
+ let instance2;
1049
+ try {
1050
+ instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
1051
+ bank.buildDispatchCreateOptions(instanceId, accountId)
1052
+ );
1053
+ } catch (err) {
1054
+ if (!bank.isAlreadyExists(err)) throw err;
1055
+ instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(instanceId);
1056
+ await instance2.restart();
1057
+ }
1058
+ return {
1059
+ instanceId: instance2.id,
1060
+ details: await instance2.status()
1061
+ };
1062
+ }
1063
+ let instance;
1064
+ try {
1065
+ const existing = await this.getCurrentSyncInstance(accountId);
1066
+ await existing.restart();
1067
+ instance = existing;
1068
+ } catch (err) {
1069
+ if (!bank.isInstanceNotFound(err)) throw err;
1070
+ }
1071
+ if (!instance) {
1072
+ instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create({
1073
+ id: accountId,
1074
+ params: { accountId }
1075
+ });
1076
+ }
1083
1077
  return {
1084
1078
  instanceId: instance.id,
1085
1079
  details: await instance.status()
@@ -1417,19 +1411,10 @@ exports.BankServiceBase = class BankServiceBase extends backendSdk.develitWorker
1417
1411
  const selection = this.env.SYNC_DISPATCH_ACCOUNT_IDS;
1418
1412
  if (!selection?.trim()) return;
1419
1413
  const accounts = await getSyncCandidateAccountsQuery(this.db);
1420
- const maxIterations = bank.parseIterationBudgetParam(
1421
- this.env.SYNC_WORKFLOW_MAX_ITERATIONS
1422
- );
1423
- if (maxIterations == null) {
1424
- syncEventConsoleLogger.error(
1425
- "sync-workflow.dispatch.invalid-max-iterations",
1426
- { value: this.env.SYNC_WORKFLOW_MAX_ITERATIONS }
1427
- );
1428
- }
1429
1414
  await dispatchSyncWorkflows({
1430
1415
  entities: accounts.filter((a) => isDispatchEnabled(a.id, selection)),
1431
1416
  createInstance: (instanceId, accountId) => this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
1432
- buildDispatchCreateOptions(instanceId, accountId, maxIterations)
1417
+ bank.buildDispatchCreateOptions(instanceId, accountId)
1433
1418
  ),
1434
1419
  logger: syncEventConsoleLogger
1435
1420
  });
package/dist/base.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { uuidv4, first, buildMultiFilterConditions as buildMultiFilterConditions$1, bankAccountMetadataSchema, structuredAddressSchema, workflowInstanceStatusSchema, develitWorker, createInternalError, action, service } from '@develit-io/backend-sdk';
2
2
  import { G as tables, g as accountInsertSchema, H as relations, o as isProcessedStatus, p as isTerminalStatus, L as getNonTerminalPaymentRequestsQuery, x as toIncomingPayment, N as calculateCzechIban, j as assignAccount, u as toBatchedPayment, y as toPaymentRequestInsert, a as FinbricksClient, F as FINBRICKS_ENDPOINTS } from './shared/bank.kz-PKVi5.mjs';
3
- import { j as encrypt, d as createCredentialsResolver, i as initiateConnector, p as parseIterationBudgetParam, e as updatePaymentRequestStatusCommand, a as getPaymentRequestsByBatchIdQuery, g as getBatchByIdQuery, u as upsertBatchCommand, b as getAccountByIdQuery, k as resolveCurrentSyncInstanceId, l as importAesKey, h as createPaymentCommand } from './shared/bank.DAjqsBQN.mjs';
3
+ import { k as isAlreadyExists, D as DISPATCH_RECOVERY_WINDOW_MS, l as buildDispatchInstanceId, m as encrypt, d as createCredentialsResolver, i as initiateConnector, n as buildDispatchCreateOptions, o as isInstanceNotFound, e as updatePaymentRequestStatusCommand, a as getPaymentRequestsByBatchIdQuery, g as getBatchByIdQuery, u as upsertBatchCommand, b as getAccountByIdQuery, r as resolveCurrentSyncInstanceId, p as importAesKey, j as createPaymentCommand } from './shared/bank.D9DoIABz.mjs';
4
4
  import { eq, sql, and, like, asc, desc, inArray, gte, lte, isNull, count } from 'drizzle-orm';
5
5
  import { WorkerEntrypoint } from 'cloudflare:workers';
6
6
  import { drizzle } from 'drizzle-orm/d1';
@@ -105,67 +105,31 @@ async function heartbeatSyncWorkflows({
105
105
  );
106
106
  }
107
107
 
108
- const DISPATCH_RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
109
- function buildDispatchInstanceId(accountId, nowMs, recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS) {
110
- return `${accountId}-${Math.floor(nowMs / recoveryWindowMs)}`;
111
- }
112
- function buildDispatchCreateOptions(instanceId, accountId, maxIterations) {
113
- return {
114
- id: instanceId,
115
- // Omitted (not undefined) when unresolved: the workflow treats a missing
116
- // budget as unbounded — degraded but syncing.
117
- params: maxIterations == null ? { accountId } : { accountId, maxIterations },
118
- // Dispatch mints ~24 instances per account per day and step outputs carry
119
- // bank payloads — the default 30-day retention would pile up billable
120
- // storage for state nobody reads after success.
121
- retention: {
122
- successRetention: "1 day",
123
- errorRetention: "7 days"
124
- }
125
- };
126
- }
127
- function isAlreadyExists(err) {
128
- const message = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : "";
129
- return /already exists/i.test(message);
130
- }
131
-
132
- const DEFAULT_MIN_GRACE_MS = 3 * 60 * 1e3;
133
108
  async function dispatchSyncWorkflows({
134
109
  entities,
135
110
  createInstance,
136
111
  logger,
137
112
  now = Date.now,
138
- minGraceMs = DEFAULT_MIN_GRACE_MS,
139
113
  recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS
140
114
  }) {
141
115
  const at = now();
142
116
  await Promise.all(
143
117
  entities.map(async (entity) => {
144
118
  const { id, lastSyncAt } = entity;
145
- const staleness = assessSyncStaleness(entity, at, minGraceMs);
146
- if (staleness == null) {
119
+ if (!lastSyncAt) {
147
120
  logger.warn("sync-workflow.dispatch.sync-age-unknown", { id });
148
121
  return;
149
122
  }
150
- if (staleness.staleForMs <= staleness.thresholdMs) return;
151
123
  const instanceId = buildDispatchInstanceId(id, at, recoveryWindowMs);
152
124
  try {
153
125
  await createInstance(instanceId, id);
154
126
  logger.info("sync-workflow.dispatch.created", {
155
127
  id,
156
128
  instanceId,
157
- lastSyncAt: lastSyncAt?.toISOString() ?? null,
158
- staleForS: Math.round(staleness.staleForMs / 1e3),
159
- thresholdS: Math.round(staleness.thresholdMs / 1e3)
129
+ lastSyncAt: lastSyncAt.toISOString()
160
130
  });
161
131
  } catch (err) {
162
- if (isAlreadyExists(err)) {
163
- logger.info("sync-workflow.dispatch.already-running", {
164
- id,
165
- instanceId
166
- });
167
- return;
168
- }
132
+ if (isAlreadyExists(err)) return;
169
133
  logger.error("sync-workflow.dispatch.failed", {
170
134
  id,
171
135
  instanceId,
@@ -1027,13 +991,10 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
1027
991
  this.env.CRON_SYNC_WORKFLOW_DISPATCH
1028
992
  )) {
1029
993
  const instanceId = buildDispatchInstanceId(accountId, Date.now());
1030
- const maxIterations = parseIterationBudgetParam(
1031
- this.env.SYNC_WORKFLOW_MAX_ITERATIONS
1032
- );
1033
994
  let instance2;
1034
995
  try {
1035
996
  instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
1036
- buildDispatchCreateOptions(instanceId, accountId, maxIterations)
997
+ buildDispatchCreateOptions(instanceId, accountId)
1037
998
  );
1038
999
  } catch (err) {
1039
1000
  if (!isAlreadyExists(err)) throw err;
@@ -1076,8 +1037,41 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
1076
1037
  { successMessage: "Account sync workflow restarted" },
1077
1038
  async ({ accountId }) => {
1078
1039
  await this.setSyncEnabledOrThrow(accountId, true);
1079
- const instance = await this.getCurrentSyncInstance(accountId);
1080
- await instance.restart();
1040
+ if (isDispatchActive(
1041
+ accountId,
1042
+ this.env.SYNC_DISPATCH_ACCOUNT_IDS,
1043
+ this.env.CRON_SYNC_WORKFLOW_DISPATCH
1044
+ )) {
1045
+ const instanceId = buildDispatchInstanceId(accountId, Date.now());
1046
+ let instance2;
1047
+ try {
1048
+ instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
1049
+ buildDispatchCreateOptions(instanceId, accountId)
1050
+ );
1051
+ } catch (err) {
1052
+ if (!isAlreadyExists(err)) throw err;
1053
+ instance2 = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.get(instanceId);
1054
+ await instance2.restart();
1055
+ }
1056
+ return {
1057
+ instanceId: instance2.id,
1058
+ details: await instance2.status()
1059
+ };
1060
+ }
1061
+ let instance;
1062
+ try {
1063
+ const existing = await this.getCurrentSyncInstance(accountId);
1064
+ await existing.restart();
1065
+ instance = existing;
1066
+ } catch (err) {
1067
+ if (!isInstanceNotFound(err)) throw err;
1068
+ }
1069
+ if (!instance) {
1070
+ instance = await this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create({
1071
+ id: accountId,
1072
+ params: { accountId }
1073
+ });
1074
+ }
1081
1075
  return {
1082
1076
  instanceId: instance.id,
1083
1077
  details: await instance.status()
@@ -1415,19 +1409,10 @@ let BankServiceBase = class extends develitWorker(WorkerEntrypoint) {
1415
1409
  const selection = this.env.SYNC_DISPATCH_ACCOUNT_IDS;
1416
1410
  if (!selection?.trim()) return;
1417
1411
  const accounts = await getSyncCandidateAccountsQuery(this.db);
1418
- const maxIterations = parseIterationBudgetParam(
1419
- this.env.SYNC_WORKFLOW_MAX_ITERATIONS
1420
- );
1421
- if (maxIterations == null) {
1422
- syncEventConsoleLogger.error(
1423
- "sync-workflow.dispatch.invalid-max-iterations",
1424
- { value: this.env.SYNC_WORKFLOW_MAX_ITERATIONS }
1425
- );
1426
- }
1427
1412
  await dispatchSyncWorkflows({
1428
1413
  entities: accounts.filter((a) => isDispatchEnabled(a.id, selection)),
1429
1414
  createInstance: (instanceId, accountId) => this.env.SYNC_ACCOUNT_PAYMENTS_WORKFLOW.create(
1430
- buildDispatchCreateOptions(instanceId, accountId, maxIterations)
1415
+ buildDispatchCreateOptions(instanceId, accountId)
1431
1416
  ),
1432
1417
  logger: syncEventConsoleLogger
1433
1418
  });
@@ -4,7 +4,7 @@ const backendSdk = require('@develit-io/backend-sdk');
4
4
  const paymentDirection = require('../shared/bank.BsWBG0gb.cjs');
5
5
  const batchLifecycle = require('../shared/bank.NF8bZBy0.cjs');
6
6
  const drizzleOrm = require('drizzle-orm');
7
- const bank = require('../shared/bank.DBTtRCcf.cjs');
7
+ const bank = require('../shared/bank.DQZxSHKV.cjs');
8
8
  const cloudflare_workers = require('cloudflare:workers');
9
9
  const cloudflare_workflows = require('cloudflare:workflows');
10
10
  const d1 = require('drizzle-orm/d1');
@@ -425,18 +425,18 @@ async function pushToQueue(queue, message) {
425
425
  }
426
426
  class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
427
427
  async run(event, step) {
428
- const { accountId, maxIterations } = event.payload;
429
- const iterationBudget = bank.resolveIterationBudget(maxIterations);
428
+ const { accountId } = event.payload;
430
429
  const db = d1.drizzle(this.env.BANK_D1, { schema: paymentDirection.tables, relations: paymentDirection.relations });
431
430
  const logger = createWorkflowLogger(event.instanceId);
432
431
  if (!accountId) {
433
432
  throw new cloudflare_workflows.NonRetryableError(`Haven't obtained accountId to load.`);
434
433
  }
434
+ const deadlineMs = bank.computeWindowDeadline(event.instanceId, accountId);
435
435
  const workflowStartedAt = await step.do(
436
436
  "capture workflow start time",
437
437
  async () => Date.now()
438
438
  );
439
- for (let iteration = 0; iteration < iterationBudget; iteration++) {
439
+ for (let iteration = 0; ; iteration++) {
440
440
  const account = await step.do("load account", async () => {
441
441
  const account2 = await bank.getAccountByIdQuery(db, { accountId });
442
442
  if (!account2) {
@@ -481,6 +481,15 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
481
481
  isCatchUp: window.isCatchUp
482
482
  };
483
483
  });
484
+ if (deadlineMs != null && windowMs.dateFromMs >= deadlineMs) {
485
+ logger.info("sync.window-elapsed", {
486
+ accountId,
487
+ instanceId: event.instanceId,
488
+ iteration,
489
+ deadline: new Date(deadlineMs).toISOString()
490
+ });
491
+ return;
492
+ }
484
493
  const syncWindow = {
485
494
  dateFrom: new Date(windowMs.dateFromMs),
486
495
  dateTo: new Date(windowMs.dateToMs),
@@ -695,6 +704,12 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
695
704
  lastSyncMetadata,
696
705
  seenInstanceId: account.lastSyncMetadata?.instanceId ?? null
697
706
  }).command;
707
+ if (eventsToEmit.length) {
708
+ await pushToQueue(
709
+ this.env.QUEUE_BUS_QUEUE,
710
+ eventsToEmit
711
+ );
712
+ }
698
713
  let syncStateRows;
699
714
  if (createCommands.length) {
700
715
  const [updateResult] = await db.batch(
@@ -710,12 +725,6 @@ class BankSyncAccountPayments extends cloudflare_workers.WorkflowEntrypoint {
710
725
  instanceId: event.instanceId
711
726
  });
712
727
  }
713
- if (eventsToEmit.length) {
714
- await pushToQueue(
715
- this.env.QUEUE_BUS_QUEUE,
716
- eventsToEmit
717
- );
718
- }
719
728
  return {
720
729
  ...lastSyncMetadata,
721
730
  newLastSyncAt: effectiveWindow.dateTo
@@ -9,7 +9,6 @@ declare class BankProcessBatch extends WorkflowEntrypoint<BankEnv, Params$1> {
9
9
 
10
10
  type Params = {
11
11
  accountId: string;
12
- maxIterations?: number;
13
12
  };
14
13
  declare class BankSyncAccountPayments extends WorkflowEntrypoint<BankEnv, Params> {
15
14
  run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<void>;
@@ -9,7 +9,6 @@ declare class BankProcessBatch extends WorkflowEntrypoint<BankEnv, Params$1> {
9
9
 
10
10
  type Params = {
11
11
  accountId: string;
12
- maxIterations?: number;
13
12
  };
14
13
  declare class BankSyncAccountPayments extends WorkflowEntrypoint<BankEnv, Params> {
15
14
  run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<void>;
@@ -9,7 +9,6 @@ declare class BankProcessBatch extends WorkflowEntrypoint<BankEnv, Params$1> {
9
9
 
10
10
  type Params = {
11
11
  accountId: string;
12
- maxIterations?: number;
13
12
  };
14
13
  declare class BankSyncAccountPayments extends WorkflowEntrypoint<BankEnv, Params> {
15
14
  run(event: WorkflowEvent<Params>, step: WorkflowStep): Promise<void>;
@@ -2,7 +2,7 @@ import { first, uuidv4, asNonEmpty } from '@develit-io/backend-sdk';
2
2
  import { G as tables, H as relations, v as toBatchedPaymentFromPaymentRequest, z as toPreparedPayment, m as isPaymentCompleted } from '../shared/bank.kz-PKVi5.mjs';
3
3
  import { i as isBatchAuthorized, b as isBatchFailed, d as isBatchProcessing } from '../shared/bank.XqSw509X.mjs';
4
4
  import { sql, and, eq, inArray } from 'drizzle-orm';
5
- import { g as getBatchByIdQuery, a as getPaymentRequestsByBatchIdQuery, c as checksum, u as upsertBatchCommand, b as getAccountByIdQuery, d as createCredentialsResolver, i as initiateConnector, e as updatePaymentRequestStatusCommand, f as isSupersededBy, h as createPaymentCommand, r as resolveIterationBudget } from '../shared/bank.DAjqsBQN.mjs';
5
+ import { g as getBatchByIdQuery, a as getPaymentRequestsByBatchIdQuery, c as checksum, u as upsertBatchCommand, b as getAccountByIdQuery, d as createCredentialsResolver, i as initiateConnector, e as updatePaymentRequestStatusCommand, f as computeWindowDeadline, h as isSupersededBy, j as createPaymentCommand } from '../shared/bank.D9DoIABz.mjs';
6
6
  import { WorkflowEntrypoint } from 'cloudflare:workers';
7
7
  import { NonRetryableError } from 'cloudflare:workflows';
8
8
  import { drizzle } from 'drizzle-orm/d1';
@@ -423,18 +423,18 @@ async function pushToQueue(queue, message) {
423
423
  }
424
424
  class BankSyncAccountPayments extends WorkflowEntrypoint {
425
425
  async run(event, step) {
426
- const { accountId, maxIterations } = event.payload;
427
- const iterationBudget = resolveIterationBudget(maxIterations);
426
+ const { accountId } = event.payload;
428
427
  const db = drizzle(this.env.BANK_D1, { schema: tables, relations });
429
428
  const logger = createWorkflowLogger(event.instanceId);
430
429
  if (!accountId) {
431
430
  throw new NonRetryableError(`Haven't obtained accountId to load.`);
432
431
  }
432
+ const deadlineMs = computeWindowDeadline(event.instanceId, accountId);
433
433
  const workflowStartedAt = await step.do(
434
434
  "capture workflow start time",
435
435
  async () => Date.now()
436
436
  );
437
- for (let iteration = 0; iteration < iterationBudget; iteration++) {
437
+ for (let iteration = 0; ; iteration++) {
438
438
  const account = await step.do("load account", async () => {
439
439
  const account2 = await getAccountByIdQuery(db, { accountId });
440
440
  if (!account2) {
@@ -479,6 +479,15 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
479
479
  isCatchUp: window.isCatchUp
480
480
  };
481
481
  });
482
+ if (deadlineMs != null && windowMs.dateFromMs >= deadlineMs) {
483
+ logger.info("sync.window-elapsed", {
484
+ accountId,
485
+ instanceId: event.instanceId,
486
+ iteration,
487
+ deadline: new Date(deadlineMs).toISOString()
488
+ });
489
+ return;
490
+ }
482
491
  const syncWindow = {
483
492
  dateFrom: new Date(windowMs.dateFromMs),
484
493
  dateTo: new Date(windowMs.dateToMs),
@@ -693,6 +702,12 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
693
702
  lastSyncMetadata,
694
703
  seenInstanceId: account.lastSyncMetadata?.instanceId ?? null
695
704
  }).command;
705
+ if (eventsToEmit.length) {
706
+ await pushToQueue(
707
+ this.env.QUEUE_BUS_QUEUE,
708
+ eventsToEmit
709
+ );
710
+ }
696
711
  let syncStateRows;
697
712
  if (createCommands.length) {
698
713
  const [updateResult] = await db.batch(
@@ -708,12 +723,6 @@ class BankSyncAccountPayments extends WorkflowEntrypoint {
708
723
  instanceId: event.instanceId
709
724
  });
710
725
  }
711
- if (eventsToEmit.length) {
712
- await pushToQueue(
713
- this.env.QUEUE_BUS_QUEUE,
714
- eventsToEmit
715
- );
716
- }
717
726
  return {
718
727
  ...lastSyncMetadata,
719
728
  newLastSyncAt: effectiveWindow.dateTo
@@ -19,7 +19,6 @@ interface BankServiceVariables {
19
19
  SYNC_WORKFLOW_RESET_AFTER_ITERATIONS: string;
20
20
  CRON_SYNC_WORKFLOW_DISPATCH: string;
21
21
  SYNC_DISPATCH_ACCOUNT_IDS: string;
22
- SYNC_WORKFLOW_MAX_ITERATIONS: string;
23
22
  [key: string]: string | number | boolean;
24
23
  }
25
24
  declare const BANK_SERVICE_BINDINGS: {
@@ -19,7 +19,6 @@ interface BankServiceVariables {
19
19
  SYNC_WORKFLOW_RESET_AFTER_ITERATIONS: string;
20
20
  CRON_SYNC_WORKFLOW_DISPATCH: string;
21
21
  SYNC_DISPATCH_ACCOUNT_IDS: string;
22
- SYNC_WORKFLOW_MAX_ITERATIONS: string;
23
22
  [key: string]: string | number | boolean;
24
23
  }
25
24
  declare const BANK_SERVICE_BINDINGS: {
package/dist/service.d.ts CHANGED
@@ -19,7 +19,6 @@ interface BankServiceVariables {
19
19
  SYNC_WORKFLOW_RESET_AFTER_ITERATIONS: string;
20
20
  CRON_SYNC_WORKFLOW_DISPATCH: string;
21
21
  SYNC_DISPATCH_ACCOUNT_IDS: string;
22
- SYNC_WORKFLOW_MAX_ITERATIONS: string;
23
22
  [key: string]: string | number | boolean;
24
23
  }
25
24
  declare const BANK_SERVICE_BINDINGS: {
@@ -7,6 +7,7 @@ import 'jose';
7
7
  import '@develit-io/general-codes';
8
8
  import { createHash } from 'node:crypto';
9
9
 
10
+ const DISPATCH_RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
10
11
  function parseInstanceWindow(instanceId, accountId) {
11
12
  const prefix = `${accountId}-`;
12
13
  if (!instanceId.startsWith(prefix)) return -1;
@@ -14,6 +15,37 @@ function parseInstanceWindow(instanceId, accountId) {
14
15
  if (!/^\d+$/.test(suffix)) return -1;
15
16
  return Number(suffix);
16
17
  }
18
+ function buildDispatchInstanceId(accountId, nowMs, recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS) {
19
+ return `${accountId}-${Math.floor(nowMs / recoveryWindowMs)}`;
20
+ }
21
+ function buildDispatchCreateOptions(instanceId, accountId) {
22
+ return {
23
+ id: instanceId,
24
+ params: { accountId },
25
+ // Dispatch mints ~24 instances per account per day and step outputs carry
26
+ // bank payloads — the default 30-day retention would pile up billable
27
+ // storage for state nobody reads after success.
28
+ retention: {
29
+ successRetention: "1 day",
30
+ errorRetention: "7 days"
31
+ }
32
+ };
33
+ }
34
+ function computeWindowDeadline(instanceId, accountId, recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS) {
35
+ const window = parseInstanceWindow(instanceId, accountId);
36
+ if (window < 0) return null;
37
+ return (window + 1) * recoveryWindowMs;
38
+ }
39
+ function isAlreadyExists(err) {
40
+ return /already exists/i.test(extractMessage(err));
41
+ }
42
+ function isInstanceNotFound(err) {
43
+ return /instance.*not.?found|not.?found.*instance/i.test(extractMessage(err));
44
+ }
45
+ function extractMessage(err) {
46
+ return err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : "";
47
+ }
48
+
17
49
  function resolveCurrentSyncInstanceId(accountId, recordedInstanceId) {
18
50
  return recordedInstanceId ?? accountId;
19
51
  }
@@ -22,18 +54,6 @@ function isSupersededBy(myInstanceId, recordedInstanceId, accountId) {
22
54
  return parseInstanceWindow(recordedInstanceId, accountId) > parseInstanceWindow(myInstanceId, accountId);
23
55
  }
24
56
 
25
- function normalizeBudget(value) {
26
- if (!Number.isFinite(value) || value < 1) return void 0;
27
- return Math.floor(value);
28
- }
29
- function parseIterationBudgetParam(value) {
30
- return normalizeBudget(Number(value?.trim() || Number.NaN));
31
- }
32
- function resolveIterationBudget(maxIterations) {
33
- if (maxIterations == null) return Number.POSITIVE_INFINITY;
34
- return normalizeBudget(maxIterations) ?? Number.POSITIVE_INFINITY;
35
- }
36
-
37
57
  const createPaymentCommand = (db, { payment }) => {
38
58
  return {
39
59
  command: db.insert(tables.payment).values({
@@ -386,4 +406,4 @@ const initiateConnector = async ({
386
406
  }
387
407
  };
388
408
 
389
- export { getPaymentRequestsByBatchIdQuery as a, getAccountByIdQuery as b, checksum as c, createCredentialsResolver as d, updatePaymentRequestStatusCommand as e, isSupersededBy as f, getBatchByIdQuery as g, createPaymentCommand as h, initiateConnector as i, encrypt as j, resolveCurrentSyncInstanceId as k, importAesKey as l, parseIterationBudgetParam as p, resolveIterationBudget as r, upsertBatchCommand as u };
409
+ export { DISPATCH_RECOVERY_WINDOW_MS as D, getPaymentRequestsByBatchIdQuery as a, getAccountByIdQuery as b, checksum as c, createCredentialsResolver as d, updatePaymentRequestStatusCommand as e, computeWindowDeadline as f, getBatchByIdQuery as g, isSupersededBy as h, initiateConnector as i, createPaymentCommand as j, isAlreadyExists as k, buildDispatchInstanceId as l, encrypt as m, buildDispatchCreateOptions as n, isInstanceNotFound as o, importAesKey as p, resolveCurrentSyncInstanceId as r, upsertBatchCommand as u };
@@ -9,6 +9,7 @@ require('jose');
9
9
  require('@develit-io/general-codes');
10
10
  const node_crypto = require('node:crypto');
11
11
 
12
+ const DISPATCH_RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
12
13
  function parseInstanceWindow(instanceId, accountId) {
13
14
  const prefix = `${accountId}-`;
14
15
  if (!instanceId.startsWith(prefix)) return -1;
@@ -16,6 +17,37 @@ function parseInstanceWindow(instanceId, accountId) {
16
17
  if (!/^\d+$/.test(suffix)) return -1;
17
18
  return Number(suffix);
18
19
  }
20
+ function buildDispatchInstanceId(accountId, nowMs, recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS) {
21
+ return `${accountId}-${Math.floor(nowMs / recoveryWindowMs)}`;
22
+ }
23
+ function buildDispatchCreateOptions(instanceId, accountId) {
24
+ return {
25
+ id: instanceId,
26
+ params: { accountId },
27
+ // Dispatch mints ~24 instances per account per day and step outputs carry
28
+ // bank payloads — the default 30-day retention would pile up billable
29
+ // storage for state nobody reads after success.
30
+ retention: {
31
+ successRetention: "1 day",
32
+ errorRetention: "7 days"
33
+ }
34
+ };
35
+ }
36
+ function computeWindowDeadline(instanceId, accountId, recoveryWindowMs = DISPATCH_RECOVERY_WINDOW_MS) {
37
+ const window = parseInstanceWindow(instanceId, accountId);
38
+ if (window < 0) return null;
39
+ return (window + 1) * recoveryWindowMs;
40
+ }
41
+ function isAlreadyExists(err) {
42
+ return /already exists/i.test(extractMessage(err));
43
+ }
44
+ function isInstanceNotFound(err) {
45
+ return /instance.*not.?found|not.?found.*instance/i.test(extractMessage(err));
46
+ }
47
+ function extractMessage(err) {
48
+ return err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : "";
49
+ }
50
+
19
51
  function resolveCurrentSyncInstanceId(accountId, recordedInstanceId) {
20
52
  return recordedInstanceId ?? accountId;
21
53
  }
@@ -24,18 +56,6 @@ function isSupersededBy(myInstanceId, recordedInstanceId, accountId) {
24
56
  return parseInstanceWindow(recordedInstanceId, accountId) > parseInstanceWindow(myInstanceId, accountId);
25
57
  }
26
58
 
27
- function normalizeBudget(value) {
28
- if (!Number.isFinite(value) || value < 1) return void 0;
29
- return Math.floor(value);
30
- }
31
- function parseIterationBudgetParam(value) {
32
- return normalizeBudget(Number(value?.trim() || Number.NaN));
33
- }
34
- function resolveIterationBudget(maxIterations) {
35
- if (maxIterations == null) return Number.POSITIVE_INFINITY;
36
- return normalizeBudget(maxIterations) ?? Number.POSITIVE_INFINITY;
37
- }
38
-
39
59
  const createPaymentCommand = (db, { payment }) => {
40
60
  return {
41
61
  command: db.insert(paymentDirection.tables.payment).values({
@@ -388,7 +408,11 @@ const initiateConnector = async ({
388
408
  }
389
409
  };
390
410
 
411
+ exports.DISPATCH_RECOVERY_WINDOW_MS = DISPATCH_RECOVERY_WINDOW_MS;
412
+ exports.buildDispatchCreateOptions = buildDispatchCreateOptions;
413
+ exports.buildDispatchInstanceId = buildDispatchInstanceId;
391
414
  exports.checksum = checksum;
415
+ exports.computeWindowDeadline = computeWindowDeadline;
392
416
  exports.createCredentialsResolver = createCredentialsResolver;
393
417
  exports.createPaymentCommand = createPaymentCommand;
394
418
  exports.encrypt = encrypt;
@@ -397,9 +421,9 @@ exports.getBatchByIdQuery = getBatchByIdQuery;
397
421
  exports.getPaymentRequestsByBatchIdQuery = getPaymentRequestsByBatchIdQuery;
398
422
  exports.importAesKey = importAesKey;
399
423
  exports.initiateConnector = initiateConnector;
424
+ exports.isAlreadyExists = isAlreadyExists;
425
+ exports.isInstanceNotFound = isInstanceNotFound;
400
426
  exports.isSupersededBy = isSupersededBy;
401
- exports.parseIterationBudgetParam = parseIterationBudgetParam;
402
427
  exports.resolveCurrentSyncInstanceId = resolveCurrentSyncInstanceId;
403
- exports.resolveIterationBudget = resolveIterationBudget;
404
428
  exports.updatePaymentRequestStatusCommand = updatePaymentRequestStatusCommand;
405
429
  exports.upsertBatchCommand = upsertBatchCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@develit-services/bank",
3
- "version": "5.9.1",
3
+ "version": "6.0.0",
4
4
  "author": "Develit.io s.r.o.",
5
5
  "type": "module",
6
6
  "exports": {