slapflow 1.1.0 → 1.2.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/README.md CHANGED
@@ -162,7 +162,7 @@ const config = {
162
162
  - A broad set of [built-in conditions](SPEC.md#built-in-conditions) for comparisons, type checks, collections, and compound logic.
163
163
  - Typed PubSub bindings and delegated DOM bindings.
164
164
  - `parallel`, `latest`, `queue`, and `drop` concurrency modes with per-entity lanes.
165
- - A WebSocket bridge for forwarding selected bus events.
165
+ - A native WebSocket client that proxies socket events into the bus.
166
166
  - `core.fetch` with response parsing, cancellation, and retry backoff.
167
167
  - Normalized results, execution trace, validation, and lifecycle diagnostics such as `slapflow.run.started` and `slapflow.run.failed`.
168
168
  - Runtime variables for configuration values, templates, and expressions.
package/SPEC-RU.md CHANGED
@@ -12,23 +12,20 @@
12
12
 
13
13
  ```ts
14
14
  import {
15
- createActionsRegistry,
16
- createConditionsRegistry,
15
+ BUILTIN_ACTIONS,
16
+ BUILTIN_CONDITIONS,
17
17
  createMemoryTraceSink,
18
18
  defineErrorReporter,
19
19
  createPubSub,
20
20
  PubSub,
21
21
  createFlow,
22
- createWS,
22
+ createWebSocket,
23
23
  catchError,
24
24
  } from 'slapflow'
25
25
  ```
26
26
 
27
27
  ```ts
28
- const flow = createFlow<Context, Patch>(
29
- { config: { strategies: {} } },
30
- { context: () => ({} as Context) }
31
- )
28
+ const flow = createFlow<Context, Patch>({ config: { strategies: {} } }, { context: () => ({}) as Context })
32
29
  const runner = flow.runner
33
30
 
34
31
  runner.registerAction('jobs.execute', executeJob)
@@ -48,10 +45,7 @@ type Config = {
48
45
  guards?: Record<string, ConditionExpression>
49
46
  }
50
47
 
51
- type ConditionExpression =
52
- | boolean
53
- | [operator: string, ...args: unknown[]]
54
- | ['guard', name: string]
48
+ type ConditionExpression = boolean | [operator: string, ...args: unknown[]] | ['guard', name: string]
55
49
 
56
50
  type Strategy = {
57
51
  fn: string
@@ -85,7 +79,7 @@ const reportError = defineErrorReporter({
85
79
 
86
80
  const flow = createFlow(
87
81
  { config: { strategies: {} } },
88
- { context: () => ({} as Context), trace: true, onError: reportError }
82
+ { context: () => ({}) as Context, trace: true, onError: reportError }
89
83
  )
90
84
  ```
91
85
 
@@ -123,40 +117,34 @@ type ErrorStage = {
123
117
 
124
118
  Возвращаемое значение действия нормализуется в один итог. Соответствие:
125
119
 
126
- | Возврат | Итог |
127
- | ------------------------------------------------- | ------------------------------------------------------------ |
128
- | `undefined` / `null` | `success` |
129
- | `false` | `skipped` |
130
- | `{ type: 'skip', reason?, data? }` | `skipped` (селектор пробует следующую ветку) |
131
- | `{ type: 'stop', reason?, patch?, events? }` | `stopped` (цепочка останавливается без ошибки) |
132
- | `{ type: 'fail', reason?, data?, error? }` | `failed` (запускается `catch`, затем `onError`) |
120
+ | Возврат | Итог |
121
+ | ------------------------------------------------- | ------------------------------------------------------------- |
122
+ | `undefined` / `null` | `success` |
123
+ | `false` | `skipped` |
124
+ | `{ type: 'skip', reason?, data? }` | `skipped` (селектор пробует следующую ветку) |
125
+ | `{ type: 'stop', reason?, patch?, events? }` | `stopped` (цепочка останавливается без ошибки) |
126
+ | `{ type: 'fail', reason?, data?, error? }` | `failed` (запускается `catch`, затем `onError`) |
133
127
  | `{ context?, data?, patch?, events?, continue? }` | `success`; `continue: false` прерывает оставшиеся `then`-цели |
134
128
 
135
129
  Брошенное исключение трактуется как `fail`. Возврат `false` и `{ type: 'skip' }` эквивалентны.
136
130
 
137
131
  ## Модель реестров
138
132
 
139
- Исполнитель использует собственные реестры:
133
+ Встроенные элементы живут в двух общих константах — по одной на вид:
140
134
 
141
- ```text
142
- src/registry/
143
- actions.ts
144
- conditions.ts
135
+ ```ts
136
+ import { BUILTIN_ACTIONS, BUILTIN_CONDITIONS } from 'slapflow'
145
137
  ```
146
138
 
147
- `createActionsRegistry()` создаёт `Map`, предварительно заполненный встроенными действиями.
139
+ `BUILTIN_ACTIONS` это readonly-список `[name, action][]`, предварительно заполненный встроенными действиями; `BUILTIN_CONDITIONS` содержит встроенные условия. Каждый исполнитель получает собственный `new Map(BUILTIN_ACTIONS)` / `new Map(BUILTIN_CONDITIONS)`, поэтому регистрации остаются изолированными по исполнителям.
148
140
 
149
- `createConditionsRegistry()` создаёт `Map`, предварительно заполненный встроенными условиями.
150
-
151
- Каждый исполнитель получает собственную изменяемую копию реестра. Приложения могут переопределить любое встроенное действие или условие:
141
+ Встроенные элементы неизменяемы: `registerAction` и `registerCondition` отклоняют попытку переопределить встроенное имя.
152
142
 
153
143
  ```ts
154
144
  runner.registerAction('app.setData', customSetData)
155
- runner.registerCondition('eq', customEq)
145
+ runner.registerCondition('hasQueue', hasItems)
156
146
  ```
157
147
 
158
- Таким образом, встроенные элементы являются значениями по умолчанию, а не отдельным неизменяемым слоем.
159
-
160
148
  Проверка конфигурации обращается к реестрам через минимальный контракт `has(name)`.
161
149
 
162
150
  ## Встроенные действия
@@ -250,11 +238,11 @@ export const config = {
250
238
 
251
239
  Не-`success` итог шага меняет дальнейшее поведение в зависимости от режима:
252
240
 
253
- | Итог | `sequence` | `selector` |
254
- | -------- | ------------------------- | -------------------------- |
255
- | `skipped`| **прерывает остаток** | пробует следующую ветку |
241
+ | Итог | `sequence` | `selector` |
242
+ | --------- | --------------------- | ----------------------- |
243
+ | `skipped` | **прерывает остаток** | пробует следующую ветку |
256
244
 
257
- `sequence` — режим по умолчанию, и он прерывает оставшиеся `then`-цели на *любом* не-`success` (`skipped`, `stopped`, `failed`) — не только на сбое. Условный шаг внутри последовательности — это, таким образом, скрытый ранний выход для всего остатка. Если пропуск шага не должен рвать цепочку, заверните его в селектор с запасным `core.noop`.
245
+ `sequence` — режим по умолчанию, и он прерывает оставшиеся `then`-цели на _любом_ не-`success` (`skipped`, `stopped`, `failed`) — не только на сбое. Условный шаг внутри последовательности — это, таким образом, скрытый ранний выход для всего остатка. Если пропуск шага не должен рвать цепочку, заверните его в селектор с запасным `core.noop`.
258
246
 
259
247
  `terminal: true` останавливает `then`-цепочку после этой стратегии даже при `success`; `continue: false` в `ActionSuccess` даёт тот же эффект.
260
248
 
@@ -399,12 +387,12 @@ type BusEvent<TPayload> = {
399
387
  На тему можно подписаться по шаблону, где `*` соответствует ровно одному сегменту, разделённому точкой. Шаблонный символ не пересекает границу `.`.
400
388
 
401
389
  ```ts
402
- bus.on('hub.user.*', ({ parsed }) => {}) // hub.user.created, hub.user.deleted
403
- bus.on('hub.*.created', ({ parsed }) => {}) // hub.user.created, hub.team.created
404
- bus.on('hub.*.export', ({ parsed }) => {}) // НЕ hub.user.audit.export (один сегмент)
390
+ bus.on('hub.user.*', ({ parsed }) => {}) // hub.user.created, hub.user.deleted
391
+ bus.on('hub.*.created', ({ parsed }) => {}) // hub.user.created, hub.team.created
392
+ bus.on('hub.*.export', ({ parsed }) => {}) // НЕ hub.user.audit.export (один сегмент)
405
393
  ```
406
394
 
407
- Подписки с точным именем остаются O(1); шаблоны обрабатываются отдельно, поэтому регистрации без `*` не несут накладных затрат на сравнение. Обработчик шаблона получает `parsed` как `unknown` — сужайте тип перед использованием. Шаблоны работают в `bus.on`/`bus.off`, а также в `inboundTopics`/`outboundTopics` у `createWS`.
395
+ Подписки с точным именем остаются O(1); шаблоны обрабатываются отдельно, поэтому регистрации без `*` не несут накладных затрат на сравнение. Обработчик шаблона получает `parsed` как `unknown` — сужайте тип перед использованием. Шаблоны работают в `bus.on`/`bus.off`.
408
396
 
409
397
  ## Поток
410
398
 
@@ -489,24 +477,30 @@ type ConcurrencyOptions<TPayload> = {
489
477
 
490
478
  `defaultInput` имеет тип `{ type, value?, dataset, form? }`. `dataset` содержит все атрибуты `data-*` совпавшего элемента в виде ключей camelCase. `form` строится по ближайшему элементу `<form>`; повторяющиеся поля формы превращаются в массивы, а `File` остаётся `File`. Для `submit` значение `preventDefault` по умолчанию равно `true`; для остальных событий оно и `stopPropagation` по умолчанию равны `false`.
491
479
 
492
- ### WebSocket-мост
480
+ ### WebSocket-клиент
493
481
 
494
- `createWS` подключает шину к WebSocket-подобному транспорту. Мост принимает `createSocket`, поэтому одинаково работает с браузерным WebSocket и серверным адаптером.
482
+ `createWebSocket` открывает нативный `WebSocket` по `url` и проксирует каждое событие сокета в шину. Формат провода не предполагается — каждое событие отправляется с фиксированной темой и конвертом, где `parsed` содержит сырую полезную нагрузку.
495
483
 
496
484
  ```ts
497
- const ws = createWS({
485
+ const socket = createWebSocket({
486
+ url,
498
487
  bus,
499
- createSocket: () => new WebSocket(url),
500
- inboundTopics: ['order.created'],
501
- outboundTopics: ['slapflow.run.finished'],
502
- origin: 'worker',
503
- retry: { initialDelay: 500, maxDelay: 10_000, multiplier: 2, jitter: true, maxAttempts: 5 },
488
+ origin: 'client',
504
489
  })
505
490
 
506
- ws.start()
491
+ socket.start()
507
492
  ```
508
493
 
509
- Входящие темы проходят через явный список разрешений. Каждая запись может быть точной темой или шаблоном, где `*` соответствует одному сегменту, разделённому точкой. Мост разбирает JSON-конверт и вызывает `bus.dispatch(event)`, сохраняя `id`, `occurredAt`, `origin`, `parsed` и `serialized`; принятые входящие идентификаторы запоминаются и не отправляются обратно наружу. Исходящие темы отправляют полный JSON-конверт события, поэтому удалённый мост может передать его в шину, не создавая новый идентификатор. `maxAttempts` ограничивает reconnect; без него повторы идут бесконечно. `start`, `stop`, `reconnect` и `status` управляют жизненным циклом транспорта. Диагностические события: `slapflow.ws.connecting`, `slapflow.ws.connected`, `slapflow.ws.disconnected`, `slapflow.ws.retrying` и `slapflow.ws.message.rejected`.
494
+ ```ts
495
+ bus.on('message', ({ parsed }) => {}) // parsed = сырое сообщение (JSON-декодированное, когда возможно)
496
+ bus.on('open', ({ parsed }) => {}) // { url }
497
+ bus.on('close', ({ parsed }) => {}) // { code, reason }
498
+ bus.on('error', ({ parsed }) => {}) // { error }
499
+ ```
500
+
501
+ События сокета приходят с темой `open`, `message`, `close` или `error`. Полезная нагрузка `message` JSON-декодируется в `parsed`, если валидна, иначе остаётся сырой строкой. Фильтрация тем — забота потребителя: внутри клиента ничего не разрешается и не отклоняется. `reconnect` принимает `initialDelay`, `maxDelay`, `multiplier`, `jitter` и `maxAttempts`; без `maxAttempts` повторы идут бесконечно. `start`, `stop`, `reconnect` и `status` управляют жизненным циклом; статус — одно из `idle`, `connecting`, `connected`, `reconnecting` или `stopped`.
502
+
503
+ `createWS` помечен как deprecated и будет удалён; мигрируйте на `createWebSocket`.
510
504
 
511
505
  ## Ограничения безопасности
512
506
 
package/SPEC.md CHANGED
@@ -12,23 +12,20 @@ The package is not coupled to a UI, server framework, scheduler, or domain model
12
12
 
13
13
  ```ts
14
14
  import {
15
- createActionsRegistry,
16
- createConditionsRegistry,
15
+ BUILTIN_ACTIONS,
16
+ BUILTIN_CONDITIONS,
17
17
  createMemoryTraceSink,
18
18
  defineErrorReporter,
19
19
  createPubSub,
20
20
  PubSub,
21
21
  createFlow,
22
- createWS,
22
+ createWebSocket,
23
23
  catchError,
24
24
  } from 'slapflow'
25
25
  ```
26
26
 
27
27
  ```ts
28
- const flow = createFlow<Context, Patch>(
29
- { config: { strategies: {} } },
30
- { context: () => ({} as Context) }
31
- )
28
+ const flow = createFlow<Context, Patch>({ config: { strategies: {} } }, { context: () => ({}) as Context })
32
29
  const runner = flow.runner
33
30
 
34
31
  runner.registerAction('jobs.execute', executeJob)
@@ -48,10 +45,7 @@ type Config = {
48
45
  guards?: Record<string, ConditionExpression>
49
46
  }
50
47
 
51
- type ConditionExpression =
52
- | boolean
53
- | [operator: string, ...args: unknown[]]
54
- | ['guard', name: string]
48
+ type ConditionExpression = boolean | [operator: string, ...args: unknown[]] | ['guard', name: string]
55
49
 
56
50
  type Strategy = {
57
51
  fn: string
@@ -85,7 +79,7 @@ const reportError = defineErrorReporter({
85
79
 
86
80
  const flow = createFlow(
87
81
  { config: { strategies: {} } },
88
- { context: () => ({} as Context), trace: true, onError: reportError }
82
+ { context: () => ({}) as Context, trace: true, onError: reportError }
89
83
  )
90
84
  ```
91
85
 
@@ -123,40 +117,34 @@ If an error is recovered through `catch`, `onError` is still invoked for the ori
123
117
 
124
118
  An action's return value is normalized into one outcome. The mapping:
125
119
 
126
- | Return | Outcome |
127
- | ------------------------------------------------- | -------------------------------------------------------------- |
128
- | `undefined` / `null` | `success` |
129
- | `false` | `skipped` |
130
- | `{ type: 'skip', reason?, data? }` | `skipped` (a selector tries the next branch) |
131
- | `{ type: 'stop', reason?, patch?, events? }` | `stopped` (the chain halts without error) |
132
- | `{ type: 'fail', reason?, data?, error? }` | `failed` (`catch` runs, then `onError`) |
120
+ | Return | Outcome |
121
+ | ------------------------------------------------- | --------------------------------------------------------------- |
122
+ | `undefined` / `null` | `success` |
123
+ | `false` | `skipped` |
124
+ | `{ type: 'skip', reason?, data? }` | `skipped` (a selector tries the next branch) |
125
+ | `{ type: 'stop', reason?, patch?, events? }` | `stopped` (the chain halts without error) |
126
+ | `{ type: 'fail', reason?, data?, error? }` | `failed` (`catch` runs, then `onError`) |
133
127
  | `{ context?, data?, patch?, events?, continue? }` | `success`; `continue: false` halts the remaining `then` targets |
134
128
 
135
129
  A thrown exception is treated as `fail`. Returning `false` and `{ type: 'skip' }` are equivalent.
136
130
 
137
131
  ## Registry Model
138
132
 
139
- The runner uses runner-scoped registries:
133
+ Built-ins live in two shared constants, one per kind:
140
134
 
141
- ```text
142
- src/registry/
143
- actions.ts
144
- conditions.ts
135
+ ```ts
136
+ import { BUILTIN_ACTIONS, BUILTIN_CONDITIONS } from 'slapflow'
145
137
  ```
146
138
 
147
- `createActionsRegistry()` creates a `Map` prepopulated with built-in actions.
139
+ `BUILTIN_ACTIONS` is a readonly `[name, action][]` list prepopulated with built-in actions; `BUILTIN_CONDITIONS` holds built-in conditions. Each runner receives its own `new Map(BUILTIN_ACTIONS)` / `new Map(BUILTIN_CONDITIONS)` seed, so registrations stay isolated per runner.
148
140
 
149
- `createConditionsRegistry()` creates a `Map` prepopulated with built-in conditions.
150
-
151
- Each runner receives its own mutable registry copy. Applications can override any built-in action or condition:
141
+ Built-ins are immutable defaults: `registerAction` and `registerCondition` reject an attempt to override a built-in name.
152
142
 
153
143
  ```ts
154
144
  runner.registerAction('app.setData', customSetData)
155
- runner.registerCondition('eq', customEq)
145
+ runner.registerCondition('hasQueue', hasItems)
156
146
  ```
157
147
 
158
- Built-ins are therefore default values, not a separate immutable layer.
159
-
160
148
  Configuration validation accesses registries through the minimal `has(name)` contract.
161
149
 
162
150
  ## Built-In Actions
@@ -250,11 +238,11 @@ export const config = {
250
238
 
251
239
  A non-`success` outcome at a step changes what happens next, depending on the mode:
252
240
 
253
- | Outcome | `sequence` | `selector` |
254
- | --------- | ---------------------- | ------------------------ |
255
- | `skipped` | **interrupts the rest** | tries the next branch |
241
+ | Outcome | `sequence` | `selector` |
242
+ | --------- | ----------------------- | --------------------- |
243
+ | `skipped` | **interrupts the rest** | tries the next branch |
256
244
 
257
- `sequence` is the default mode and interrupts the remaining `then` targets on *any* non-`success` (`skipped`, `stopped`, `failed`) — not only on failure. A conditional step inside a sequence is therefore a hidden early exit for the whole remainder. When skipping a step must not break the chain, wrap it in a selector with a `core.noop` fallback.
245
+ `sequence` is the default mode and interrupts the remaining `then` targets on _any_ non-`success` (`skipped`, `stopped`, `failed`) — not only on failure. A conditional step inside a sequence is therefore a hidden early exit for the whole remainder. When skipping a step must not break the chain, wrap it in a selector with a `core.noop` fallback.
258
246
 
259
247
  `terminal: true` stops the `then` chain after that strategy even on `success`; `continue: false` in `ActionSuccess` has the same effect.
260
248
 
@@ -399,12 +387,12 @@ type BusEvent<TPayload> = {
399
387
  A topic may be subscribed by pattern, using `*` to match exactly one dot-delimited segment. A wildcard does not cross a `.` boundary.
400
388
 
401
389
  ```ts
402
- bus.on('hub.user.*', ({ parsed }) => {}) // hub.user.created, hub.user.deleted
403
- bus.on('hub.*.created', ({ parsed }) => {}) // hub.user.created, hub.team.created
404
- bus.on('hub.*.export', ({ parsed }) => {}) // NOT hub.user.audit.export (wildcard spans one segment)
390
+ bus.on('hub.user.*', ({ parsed }) => {}) // hub.user.created, hub.user.deleted
391
+ bus.on('hub.*.created', ({ parsed }) => {}) // hub.user.created, hub.team.created
392
+ bus.on('hub.*.export', ({ parsed }) => {}) // NOT hub.user.audit.export (wildcard spans one segment)
405
393
  ```
406
394
 
407
- Exact-name subscriptions stay O(1); wildcard patterns are matched separately, so registrations without `*` pay no matching cost. A wildcard handler receives `parsed` as `unknown` — narrow it before use. Wildcards work in `bus.on`/`bus.off` and in `inboundTopics`/`outboundTopics` of `createWS`.
395
+ Exact-name subscriptions stay O(1); wildcard patterns are matched separately, so registrations without `*` pay no matching cost. A wildcard handler receives `parsed` as `unknown` — narrow it before use. Wildcards work in `bus.on`/`bus.off`.
408
396
 
409
397
  ## Flow
410
398
 
@@ -489,24 +477,30 @@ A DOM binding key uses the `[dom] <css-selector>:<event>` format. Slapflow insta
489
477
 
490
478
  `defaultInput` has type `{ type, value?, dataset, form? }`. `dataset` contains all `data-*` attributes from the matching element as camelCase keys. `form` is built from the nearest `<form>`; repeated form entries become arrays and `File` remains `File`. For `submit`, `preventDefault` defaults to `true`; for other events, it and `stopPropagation` default to `false`.
491
479
 
492
- ### WebSocket Bridge
480
+ ### WebSocket Client
493
481
 
494
- `createWS` connects a bus to a WebSocket-like transport. The bridge accepts `createSocket`, so it works with browser WebSocket and a server adapter alike.
482
+ `createWebSocket` opens a native `WebSocket` to `url` and proxies every socket event into the bus. No wire format is assumed each event is dispatched with a fixed topic and an envelope whose `parsed` holds the raw payload.
495
483
 
496
484
  ```ts
497
- const ws = createWS({
485
+ const socket = createWebSocket({
486
+ url,
498
487
  bus,
499
- createSocket: () => new WebSocket(url),
500
- inboundTopics: ['order.created'],
501
- outboundTopics: ['slapflow.run.finished'],
502
- origin: 'worker',
503
- retry: { initialDelay: 500, maxDelay: 10_000, multiplier: 2, jitter: true, maxAttempts: 5 },
488
+ origin: 'client',
504
489
  })
505
490
 
506
- ws.start()
491
+ socket.start()
507
492
  ```
508
493
 
509
- Inbound topics pass an explicit allowlist. Each entry may be an exact topic or a wildcard pattern, where `*` matches one dot-delimited segment. The bridge parses a JSON envelope and calls `bus.dispatch(event)`, preserving `id`, `occurredAt`, `origin`, `parsed`, and `serialized`; it remembers accepted inbound IDs and does not send them back outbound. Outbound topics send the complete event envelope as JSON, so the remote bridge can dispatch it without recreating its identity. `maxAttempts` limits reconnects; omitting it retries indefinitely. `start`, `stop`, `reconnect`, and `status` manage the transport lifecycle. Diagnostics: `slapflow.ws.connecting`, `slapflow.ws.connected`, `slapflow.ws.disconnected`, `slapflow.ws.retrying`, and `slapflow.ws.message.rejected`.
494
+ ```ts
495
+ bus.on('message', ({ parsed }) => {}) // parsed = the raw message (JSON-decoded when possible)
496
+ bus.on('open', ({ parsed }) => {}) // { url }
497
+ bus.on('close', ({ parsed }) => {}) // { code, reason }
498
+ bus.on('error', ({ parsed }) => {}) // { error }
499
+ ```
500
+
501
+ Socket events are forwarded with the topic `open`, `message`, `close`, or `error`. `message` payloads are JSON-decoded into `parsed` when valid, otherwise kept as the raw string. Filtering topics is the consumer's responsibility — nothing is allow-listed or rejected inside the client. `reconnect` accepts `initialDelay`, `maxDelay`, `multiplier`, `jitter`, and `maxAttempts`; omitting `maxAttempts` retries indefinitely. `start`, `stop`, `reconnect`, and `status` manage the lifecycle; status is one of `idle`, `connecting`, `connected`, `reconnecting`, or `stopped`.
502
+
503
+ `createWS` is deprecated and will be removed; migrate to `createWebSocket`.
510
504
 
511
505
  ## Safety Limits
512
506
 
package/dist/index.cjs CHANGED
@@ -20,14 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ BUILTIN_ACTIONS: () => BUILTIN_ACTIONS,
24
+ BUILTIN_ACTION_NAMES: () => BUILTIN_ACTION_NAMES,
25
+ BUILTIN_CONDITIONS: () => BUILTIN_CONDITIONS,
26
+ BUILTIN_CONDITION_NAMES: () => BUILTIN_CONDITION_NAMES,
23
27
  PubSub: () => PubSub,
24
28
  catchError: () => catchError,
25
- createActionsRegistry: () => createActionsRegistry,
26
- createConditionsRegistry: () => createConditionsRegistry,
27
29
  createFlow: () => createFlow,
28
30
  createMemoryTraceSink: () => createMemoryTraceSink,
29
31
  createPubSub: () => createPubSub,
30
32
  createWS: () => createWS,
33
+ createWebSocket: () => createWebSocket,
31
34
  defineConfig: () => defineConfig,
32
35
  defineErrorReporter: () => defineErrorReporter
33
36
  });
@@ -36,9 +39,6 @@ module.exports = __toCommonJS(index_exports);
36
39
  // src/helpers/config/defineConfig.ts
37
40
  var defineConfig = (config) => config;
38
41
 
39
- // src/helpers/trace/cloneData.ts
40
- var cloneData = (data) => ({ ...data });
41
-
42
42
  // src/helpers/trace/createMemoryTraceSink.ts
43
43
  var createMemoryTraceSink = () => {
44
44
  const items = [];
@@ -50,22 +50,9 @@ var createMemoryTraceSink = () => {
50
50
  };
51
51
  };
52
52
 
53
- // src/helpers/errors/slapError.ts
54
- var slapError = (code, message, extras = {}) => ({ code, message, ...extras });
55
-
56
53
  // src/helpers/errors/defineErrorReporter.ts
57
54
  var defineErrorReporter = (handlers) => typeof handlers === "function" ? handlers : handlers.report;
58
55
 
59
- // src/errors.ts
60
- var SyncAsyncError = class extends Error {
61
- slapError;
62
- constructor(error) {
63
- super(error.message);
64
- this.name = "SyncAsyncError";
65
- this.slapError = error;
66
- }
67
- };
68
-
69
56
  // src/helpers/ids/createId.ts
70
57
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
71
58
  var idLength = 12;
@@ -105,7 +92,7 @@ var serializeError = (error) => ({
105
92
  error: error instanceof Error ? error.message : String(error)
106
93
  });
107
94
 
108
- // src/pubSub.ts
95
+ // src/createPubSub.ts
109
96
  var createPubSub = (options = {}) => {
110
97
  const subscribers = /* @__PURE__ */ new Map();
111
98
  const wildcardSubscribers = /* @__PURE__ */ new Map();
@@ -203,6 +190,16 @@ var createPubSub = (options = {}) => {
203
190
  };
204
191
  var PubSub = createPubSub();
205
192
 
193
+ // src/helpers/errors/syncAsyncError.ts
194
+ var SyncAsyncError = class extends Error {
195
+ slapError;
196
+ constructor(error) {
197
+ super(error.message);
198
+ this.name = "SyncAsyncError";
199
+ this.slapError = error;
200
+ }
201
+ };
202
+
206
203
  // src/helpers/actions/coreDelay.ts
207
204
  var coreDelay = async ({
208
205
  props,
@@ -239,10 +236,7 @@ var coreEmit = ({
239
236
  };
240
237
 
241
238
  // src/helpers/actions/coreFail.ts
242
- var coreFail = ({
243
- props,
244
- runtime
245
- }) => runtime.fail(String(props.reason ?? "failed"), props.data);
239
+ var coreFail = ({ props, runtime }) => runtime.fail(String(props.reason ?? "failed"), props.data);
246
240
 
247
241
  // src/helpers/retry/getRetryDelay.ts
248
242
  var getRetryDelay = (attempt, options) => {
@@ -449,10 +443,7 @@ var corePatch = ({
449
443
  };
450
444
 
451
445
  // src/helpers/actions/coreSet.ts
452
- var coreSet = ({
453
- props,
454
- runtime
455
- }) => {
446
+ var coreSet = ({ props, runtime }) => {
456
447
  const path = props.path;
457
448
  if (typeof path === "string") {
458
449
  runtime.set(path, props.value);
@@ -473,13 +464,10 @@ var coreSetData = ({
473
464
  };
474
465
 
475
466
  // src/helpers/actions/coreStop.ts
476
- var coreStop = ({
477
- props,
478
- runtime
479
- }) => runtime.stop(String(props.reason ?? "stopped"));
467
+ var coreStop = ({ props, runtime }) => runtime.stop(String(props.reason ?? "stopped"));
480
468
 
481
- // src/registry/actions.ts
482
- var createActionsRegistry = () => /* @__PURE__ */ new Map([
469
+ // src/helpers/actions/index.ts
470
+ var BUILTIN_ACTIONS = [
483
471
  ["core.noop", coreNoop],
484
472
  ["core.stop", coreStop],
485
473
  ["core.fail", coreFail],
@@ -493,7 +481,8 @@ var createActionsRegistry = () => /* @__PURE__ */ new Map([
493
481
  ["core.emit", coreEmit],
494
482
  ["core.patch", corePatch],
495
483
  ["core.delay", coreDelay]
496
- ]);
484
+ ];
485
+ var BUILTIN_ACTION_NAMES = new Set(BUILTIN_ACTIONS.map(([name]) => name));
497
486
 
498
487
  // src/helpers/conditions/changedCondition.ts
499
488
  var changedCondition = (_args, current, previous) => !Object.is(current, previous);
@@ -587,8 +576,8 @@ var typeIsCondition = (_args, value, expected) => {
587
576
  return expected === "string" || expected === "number" || expected === "boolean" ? typeof value === expected : false;
588
577
  };
589
578
 
590
- // src/registry/conditions.ts
591
- var createConditionsRegistry = () => /* @__PURE__ */ new Map([
579
+ // src/helpers/conditions/index.ts
580
+ var BUILTIN_CONDITIONS = [
592
581
  ["eq", eqCondition],
593
582
  ["neq", neqCondition],
594
583
  ["gt", gtCondition],
@@ -605,7 +594,8 @@ var createConditionsRegistry = () => /* @__PURE__ */ new Map([
605
594
  ["typeIs", typeIsCondition],
606
595
  ["changed", changedCondition],
607
596
  ["cooldownReady", cooldownReadyCondition]
608
- ]);
597
+ ];
598
+ var BUILTIN_CONDITION_NAMES = new Set(BUILTIN_CONDITIONS.map(([name]) => name));
609
599
 
610
600
  // src/helpers/runner/applyResult.ts
611
601
  var applyResult = (result, state, mergeData) => {
@@ -619,6 +609,9 @@ var applyResult = (result, state, mergeData) => {
619
609
  state.events.push(...result.events);
620
610
  };
621
611
 
612
+ // src/helpers/errors/slapError.ts
613
+ var slapError = (code, message, extras = {}) => ({ code, message, ...extras });
614
+
622
615
  // src/helpers/path/resolveValue.ts
623
616
  var import_objwalk3 = require("objwalk");
624
617
 
@@ -1057,7 +1050,7 @@ var executeNext = (item, depth, state, environment) => {
1057
1050
  const id = typeof item === "string" ? item : item.strategy;
1058
1051
  if (typeof item !== "string" && item.when) {
1059
1052
  const runtime = createRuntime(state);
1060
- const condition = evaluateCondition(item.when, environment.conditionsRegistry, { ...state, runtime, strategy: id });
1053
+ const condition = evaluateCondition(item.when, environment.registry.conditions, { ...state, runtime, strategy: id });
1061
1054
  if (!condition.ok) {
1062
1055
  return { status: "failed", error: condition.error, patches: [], events: [] };
1063
1056
  }
@@ -1309,6 +1302,9 @@ var normalizeActionResult = (raw) => {
1309
1302
  };
1310
1303
  };
1311
1304
 
1305
+ // src/helpers/trace/cloneData.ts
1306
+ var cloneData = (data) => ({ ...data });
1307
+
1312
1308
  // src/helpers/runner/pushTrace.ts
1313
1309
  var pushTrace = (state, step, depth, strategyId, strategy, status, props, dataBefore, startedAt, reason) => {
1314
1310
  state.traceSink?.push({
@@ -1491,7 +1487,7 @@ var executeStrategy = (id, extraProps, depth, state, environment) => {
1491
1487
  events: []
1492
1488
  };
1493
1489
  }
1494
- const action = environment.actionsRegistry.get(strategy.fn);
1490
+ const action = environment.registry.actions.get(strategy.fn);
1495
1491
  if (!action) {
1496
1492
  return {
1497
1493
  status: "failed",
@@ -1546,7 +1542,7 @@ var executeStrategy = (id, extraProps, depth, state, environment) => {
1546
1542
  const dataBefore = cloneData(state.data);
1547
1543
  const traceStep = state.stepCounter.current + 1;
1548
1544
  const startedAt = Date.now();
1549
- const condition = evaluateCondition(strategy.when, environment.conditionsRegistry, {
1545
+ const condition = evaluateCondition(strategy.when, environment.registry.conditions, {
1550
1546
  ...state,
1551
1547
  runtime,
1552
1548
  strategy: id
@@ -1860,7 +1856,12 @@ var validateCondition = (expression, strategy, path, conditionsRegistry, errors)
1860
1856
  const [operator, ...args] = expression;
1861
1857
  if (operator === "guard") {
1862
1858
  if (args.length !== 1 || typeof args[0] !== "string") {
1863
- errors.push({ code: "CONDITION_INVALID", message: "Guard reference must be a single string name", strategy, path });
1859
+ errors.push({
1860
+ code: "CONDITION_INVALID",
1861
+ message: "Guard reference must be a single string name",
1862
+ strategy,
1863
+ path
1864
+ });
1864
1865
  }
1865
1866
  return;
1866
1867
  }
@@ -1913,7 +1914,9 @@ var validateNextList = (config, list, path, strategy, conditionsRegistry, errors
1913
1914
  var isGuardRef = (expression) => Array.isArray(expression) && expression.length === 2 && expression[0] === "guard" && typeof expression[1] === "string";
1914
1915
  var resolveRef = (config, name, visiting, path) => {
1915
1916
  if (visiting.includes(name)) {
1916
- return { issue: { code: "GUARD_CYCLE", message: `Guard "${name}" is part of a reference cycle`, guard: name, path } };
1917
+ return {
1918
+ issue: { code: "GUARD_CYCLE", message: `Guard "${name}" is part of a reference cycle`, guard: name, path }
1919
+ };
1917
1920
  }
1918
1921
  const guard = config.guards?.[name];
1919
1922
  if (guard === void 0) {
@@ -2136,10 +2139,10 @@ var createRunCancellation = (source) => {
2136
2139
  };
2137
2140
  };
2138
2141
 
2139
- // src/helpers/runner/createRunner.ts
2142
+ // src/createRunner.ts
2140
2143
  var createRunner = (options = {}) => {
2141
- const actionsRegistry = createActionsRegistry();
2142
- const conditionsRegistry = createConditionsRegistry();
2144
+ const actionsRegistry = new Map(BUILTIN_ACTIONS);
2145
+ const conditionsRegistry = new Map(BUILTIN_CONDITIONS);
2143
2146
  const configRef = {};
2144
2147
  const timeout = options.timeout ?? options.timeoutMs;
2145
2148
  const runnerOptions = timeout === void 0 ? options : { ...options, timeout };
@@ -2149,19 +2152,24 @@ var createRunner = (options = {}) => {
2149
2152
  console.warn("timeoutMs is deprecated; use timeout. It will be removed in a future major release.");
2150
2153
  }
2151
2154
  const environment = {
2152
- actionsRegistry,
2153
- conditionsRegistry,
2155
+ registry: { actions: actionsRegistry, conditions: conditionsRegistry },
2154
2156
  configRef,
2155
2157
  options: runnerOptions,
2156
2158
  mergeData
2157
2159
  };
2158
2160
  const registerAction = (name, action) => {
2161
+ if (BUILTIN_ACTION_NAMES.has(name)) {
2162
+ throw new Error(`Cannot override built-in action "${name}"`);
2163
+ }
2159
2164
  actionsRegistry.set(name, action);
2160
2165
  };
2161
2166
  const registerActions = (items) => {
2162
2167
  Object.entries(items).forEach(([name, action]) => registerAction(name, action));
2163
2168
  };
2164
2169
  const registerCondition = (name, condition) => {
2170
+ if (BUILTIN_CONDITION_NAMES.has(name)) {
2171
+ throw new Error(`Cannot override built-in condition "${name}"`);
2172
+ }
2165
2173
  conditionsRegistry.set(name, condition);
2166
2174
  };
2167
2175
  const registerConditions = (items) => {
@@ -2258,7 +2266,7 @@ var parseDomBinding = (binding, prefix) => {
2258
2266
  return separator <= 0 || separator === source.length - 1 ? void 0 : { selector: source.slice(0, separator), eventType: source.slice(separator + 1) };
2259
2267
  };
2260
2268
 
2261
- // src/flow.ts
2269
+ // src/createFlow.ts
2262
2270
  var busBindingPrefix = "[bus] ";
2263
2271
  var domBindingPrefix = "[dom] ";
2264
2272
  var defaultMaxQueueSize = 50;
@@ -2499,10 +2507,11 @@ var createFlow = (definition, options) => {
2499
2507
  return { runner, start, stop };
2500
2508
  };
2501
2509
 
2502
- // src/ws.ts
2510
+ // src/createWS.ts
2503
2511
  var openState = 1;
2504
2512
  var maxSeenEvents = 1e3;
2505
2513
  var createWS = (options) => {
2514
+ console.warn("[slapflow] createWS is deprecated and will be removed soon. Use createWebSocket instead.");
2506
2515
  const inboundTopics = new Set(options.inboundTopics ?? []);
2507
2516
  const outboundTopics = new Set(options.outboundTopics ?? []);
2508
2517
  const seenEventIds = /* @__PURE__ */ new Set();
@@ -2666,7 +2675,117 @@ var createWS = (options) => {
2666
2675
  return { start, stop, reconnect, status: () => currentStatus };
2667
2676
  };
2668
2677
 
2669
- // src/catchError.ts
2678
+ // src/createWebSocket.ts
2679
+ var createWebSocket = (options) => {
2680
+ let socket;
2681
+ let retryTimer;
2682
+ let retryAttempt = 0;
2683
+ let started = false;
2684
+ let currentStatus = "idle";
2685
+ const emitSocketEvent = (topic, payload) => {
2686
+ options.bus.dispatch({
2687
+ id: createId(),
2688
+ topic,
2689
+ occurredAt: Date.now(),
2690
+ ...options.origin ? { origin: options.origin } : {},
2691
+ parsed: payload,
2692
+ serialized: JSON.stringify(payload)
2693
+ });
2694
+ };
2695
+ const scheduleRetry = () => {
2696
+ const delay = getRetryDelay(retryAttempt, options.reconnect ?? {});
2697
+ const attempt = retryAttempt + 1;
2698
+ currentStatus = "reconnecting";
2699
+ retryAttempt = attempt;
2700
+ retryTimer = setTimeout(() => {
2701
+ retryTimer = void 0;
2702
+ connect();
2703
+ }, delay);
2704
+ };
2705
+ const connect = () => {
2706
+ if (!started || socket) {
2707
+ return;
2708
+ }
2709
+ currentStatus = "connecting";
2710
+ try {
2711
+ const current = new WebSocket(options.url, options.protocols ?? []);
2712
+ socket = current;
2713
+ current.addEventListener("open", () => {
2714
+ if (socket === current) {
2715
+ retryAttempt = 0;
2716
+ currentStatus = "connected";
2717
+ emitSocketEvent("open", { url: options.url });
2718
+ }
2719
+ });
2720
+ current.addEventListener("message", (event) => {
2721
+ if (socket !== current) {
2722
+ return;
2723
+ }
2724
+ let parsed = event.data;
2725
+ if (typeof parsed === "string") {
2726
+ try {
2727
+ parsed = JSON.parse(parsed);
2728
+ } catch {
2729
+ parsed = event.data;
2730
+ }
2731
+ }
2732
+ emitSocketEvent("message", parsed);
2733
+ });
2734
+ current.addEventListener("close", (event) => {
2735
+ if (socket !== current) {
2736
+ return;
2737
+ }
2738
+ socket = void 0;
2739
+ emitSocketEvent("close", { code: event.code, reason: event.reason });
2740
+ if (started) {
2741
+ scheduleRetry();
2742
+ }
2743
+ });
2744
+ current.addEventListener("error", (event) => {
2745
+ emitSocketEvent("error", { error: event });
2746
+ });
2747
+ } catch (error) {
2748
+ socket = void 0;
2749
+ emitSocketEvent("error", { error });
2750
+ if (started) {
2751
+ scheduleRetry();
2752
+ }
2753
+ }
2754
+ };
2755
+ const start = () => {
2756
+ if (!started) {
2757
+ started = true;
2758
+ connect();
2759
+ }
2760
+ };
2761
+ const stop = () => {
2762
+ started = false;
2763
+ if (retryTimer) {
2764
+ clearTimeout(retryTimer);
2765
+ }
2766
+ retryTimer = void 0;
2767
+ const current = socket;
2768
+ socket = void 0;
2769
+ current?.close();
2770
+ currentStatus = "stopped";
2771
+ };
2772
+ const reconnect = () => {
2773
+ if (!started) {
2774
+ return;
2775
+ }
2776
+ if (retryTimer) {
2777
+ clearTimeout(retryTimer);
2778
+ retryTimer = void 0;
2779
+ }
2780
+ const current = socket;
2781
+ socket = void 0;
2782
+ current?.close();
2783
+ connect();
2784
+ };
2785
+ return { start, stop, reconnect, status: () => currentStatus };
2786
+ };
2787
+
2788
+ // src/helpers/catchError.ts
2670
2789
  var catchError = (callback) => new Promise((resolve, reject) => {
2671
2790
  try {
2672
2791
  resolve(callback());
@@ -2676,14 +2795,17 @@ var catchError = (callback) => new Promise((resolve, reject) => {
2676
2795
  });
2677
2796
  // Annotate the CommonJS export names for ESM import in node:
2678
2797
  0 && (module.exports = {
2798
+ BUILTIN_ACTIONS,
2799
+ BUILTIN_ACTION_NAMES,
2800
+ BUILTIN_CONDITIONS,
2801
+ BUILTIN_CONDITION_NAMES,
2679
2802
  PubSub,
2680
2803
  catchError,
2681
- createActionsRegistry,
2682
- createConditionsRegistry,
2683
2804
  createFlow,
2684
2805
  createMemoryTraceSink,
2685
2806
  createPubSub,
2686
2807
  createWS,
2808
+ createWebSocket,
2687
2809
  defineConfig,
2688
2810
  defineErrorReporter
2689
2811
  });
package/dist/index.d.cts CHANGED
@@ -148,6 +148,21 @@ type WS = {
148
148
  reconnect(): void;
149
149
  status(): WSStatus;
150
150
  };
151
+ type SocketEventTopic = 'open' | 'message' | 'close' | 'error';
152
+ type WebSocketOptions<TEvents extends object = EventMap> = {
153
+ url: string;
154
+ bus: Bus<TEvents>;
155
+ origin?: string;
156
+ protocols?: string | string[];
157
+ reconnect?: RetryOptions;
158
+ };
159
+ type WebSocketStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'stopped';
160
+ type WSClient = {
161
+ start(): void;
162
+ stop(): void;
163
+ reconnect(): void;
164
+ status(): WebSocketStatus;
165
+ };
151
166
  type BindingEventMap = Record<string, Input>;
152
167
  type BusBindingKey<TEvents extends object> = {
153
168
  [TEvent in EventName<TEvents>]: TEvents[TEvent] extends Input ? `[bus] ${TEvent}` : never;
@@ -374,14 +389,21 @@ declare const PubSub: Bus<EventMap>;
374
389
 
375
390
  declare const createFlow: <TContext, TPatch = unknown, TEvents extends object = BindingEventMap>(definition: FlowDefinition<TContext, TPatch, TEvents>, options: FlowOptions<TContext, TPatch, TEvents>) => Flow<TContext, TPatch>;
376
391
 
392
+ /**
393
+ * @deprecated Use `createWebSocket` instead. This adapter will be removed in a future release.
394
+ */
377
395
  declare const createWS: <TEvents extends object = EventMap>(options: WSOptions<TEvents>) => WS;
378
396
 
397
+ declare const createWebSocket: <TEvents extends object = EventMap>(options: WebSocketOptions<TEvents>) => WSClient;
398
+
379
399
  declare const catchError: <T>(callback: () => T | PromiseLike<T>) => Promise<T>;
380
400
 
381
401
  type ActionsRegistry<TContext, TPatch> = Map<string, Action<TContext, TPatch>>;
382
- declare const createActionsRegistry: <TContext, TPatch>() => ActionsRegistry<TContext, TPatch>;
402
+ declare const BUILTIN_ACTIONS: readonly [name: string, action: Action<unknown, unknown>][];
403
+ declare const BUILTIN_ACTION_NAMES: Set<string>;
383
404
 
384
405
  type ConditionsRegistry<TContext> = Map<string, ConditionFn<TContext>>;
385
- declare const createConditionsRegistry: <TContext>() => ConditionsRegistry<TContext>;
406
+ declare const BUILTIN_CONDITIONS: readonly [name: string, condition: ConditionFn<unknown>][];
407
+ declare const BUILTIN_CONDITION_NAMES: Set<string>;
386
408
 
387
- export { type Action, type ActionArgs, type ActionFail, type ActionResult, type ActionSkip, type ActionStop, type ActionSuccess, type ActionsRegistry, type BindingEventMap, type Bus, type BusBinding, type BusBindingKey, type BusBindings, type BusEmitOptions, type BusErrorEvent, type BusEvent, type BusOptions, type ConcurrencyMode, type ConcurrencyOptions, type ConditionExpression, type ConditionFn, type ConditionsRegistry, type Config, type DomBinding, type DomBindingKey, type DomBindings, type DomForm, type DomInput, type ErrorReporter, type ErrorReporterHandlers, type ErrorStage, type EventHandler, type EventMap, type EventName, type EventPattern, type ExpressionOperator, type FetchResponseType, type Flow, type FlowDefinition, type FlowOptions, type InactiveBinding, type Input, type Mode, type Next, type Props, PubSub, type QueueOverflow, type RetryOptions, type RunOptions, type RunResult, type Runner, type RunnerErrorEvent, type RunnerOptions, type Runtime, type SlapError, type SlapErrorEvent, type SlapEvent, type StartResult, type Strategy, type TraceEntry, type TraceSink, type ValidationIssue, type ValidationResult, type VariableValue, type Variables, type WS, type WSOptions, type WSRetryOptions, type WSSocket, type WSStatus, catchError, createActionsRegistry, createConditionsRegistry, createFlow, createMemoryTraceSink, createPubSub, createWS, defineConfig, defineErrorReporter };
409
+ export { type Action, type ActionArgs, type ActionFail, type ActionResult, type ActionSkip, type ActionStop, type ActionSuccess, type ActionsRegistry, BUILTIN_ACTIONS, BUILTIN_ACTION_NAMES, BUILTIN_CONDITIONS, BUILTIN_CONDITION_NAMES, type BindingEventMap, type Bus, type BusBinding, type BusBindingKey, type BusBindings, type BusEmitOptions, type BusErrorEvent, type BusEvent, type BusOptions, type ConcurrencyMode, type ConcurrencyOptions, type ConditionExpression, type ConditionFn, type ConditionsRegistry, type Config, type DomBinding, type DomBindingKey, type DomBindings, type DomForm, type DomInput, type ErrorReporter, type ErrorReporterHandlers, type ErrorStage, type EventHandler, type EventMap, type EventName, type EventPattern, type ExpressionOperator, type FetchResponseType, type Flow, type FlowDefinition, type FlowOptions, type InactiveBinding, type Input, type Mode, type Next, type Props, PubSub, type QueueOverflow, type RetryOptions, type RunOptions, type RunResult, type Runner, type RunnerErrorEvent, type RunnerOptions, type Runtime, type SlapError, type SlapErrorEvent, type SlapEvent, type SocketEventTopic, type StartResult, type Strategy, type TraceEntry, type TraceSink, type ValidationIssue, type ValidationResult, type VariableValue, type Variables, type WS, type WSClient, type WSOptions, type WSRetryOptions, type WSSocket, type WSStatus, type WebSocketOptions, type WebSocketStatus, catchError, createFlow, createMemoryTraceSink, createPubSub, createWS, createWebSocket, defineConfig, defineErrorReporter };
package/dist/index.d.ts CHANGED
@@ -148,6 +148,21 @@ type WS = {
148
148
  reconnect(): void;
149
149
  status(): WSStatus;
150
150
  };
151
+ type SocketEventTopic = 'open' | 'message' | 'close' | 'error';
152
+ type WebSocketOptions<TEvents extends object = EventMap> = {
153
+ url: string;
154
+ bus: Bus<TEvents>;
155
+ origin?: string;
156
+ protocols?: string | string[];
157
+ reconnect?: RetryOptions;
158
+ };
159
+ type WebSocketStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'stopped';
160
+ type WSClient = {
161
+ start(): void;
162
+ stop(): void;
163
+ reconnect(): void;
164
+ status(): WebSocketStatus;
165
+ };
151
166
  type BindingEventMap = Record<string, Input>;
152
167
  type BusBindingKey<TEvents extends object> = {
153
168
  [TEvent in EventName<TEvents>]: TEvents[TEvent] extends Input ? `[bus] ${TEvent}` : never;
@@ -374,14 +389,21 @@ declare const PubSub: Bus<EventMap>;
374
389
 
375
390
  declare const createFlow: <TContext, TPatch = unknown, TEvents extends object = BindingEventMap>(definition: FlowDefinition<TContext, TPatch, TEvents>, options: FlowOptions<TContext, TPatch, TEvents>) => Flow<TContext, TPatch>;
376
391
 
392
+ /**
393
+ * @deprecated Use `createWebSocket` instead. This adapter will be removed in a future release.
394
+ */
377
395
  declare const createWS: <TEvents extends object = EventMap>(options: WSOptions<TEvents>) => WS;
378
396
 
397
+ declare const createWebSocket: <TEvents extends object = EventMap>(options: WebSocketOptions<TEvents>) => WSClient;
398
+
379
399
  declare const catchError: <T>(callback: () => T | PromiseLike<T>) => Promise<T>;
380
400
 
381
401
  type ActionsRegistry<TContext, TPatch> = Map<string, Action<TContext, TPatch>>;
382
- declare const createActionsRegistry: <TContext, TPatch>() => ActionsRegistry<TContext, TPatch>;
402
+ declare const BUILTIN_ACTIONS: readonly [name: string, action: Action<unknown, unknown>][];
403
+ declare const BUILTIN_ACTION_NAMES: Set<string>;
383
404
 
384
405
  type ConditionsRegistry<TContext> = Map<string, ConditionFn<TContext>>;
385
- declare const createConditionsRegistry: <TContext>() => ConditionsRegistry<TContext>;
406
+ declare const BUILTIN_CONDITIONS: readonly [name: string, condition: ConditionFn<unknown>][];
407
+ declare const BUILTIN_CONDITION_NAMES: Set<string>;
386
408
 
387
- export { type Action, type ActionArgs, type ActionFail, type ActionResult, type ActionSkip, type ActionStop, type ActionSuccess, type ActionsRegistry, type BindingEventMap, type Bus, type BusBinding, type BusBindingKey, type BusBindings, type BusEmitOptions, type BusErrorEvent, type BusEvent, type BusOptions, type ConcurrencyMode, type ConcurrencyOptions, type ConditionExpression, type ConditionFn, type ConditionsRegistry, type Config, type DomBinding, type DomBindingKey, type DomBindings, type DomForm, type DomInput, type ErrorReporter, type ErrorReporterHandlers, type ErrorStage, type EventHandler, type EventMap, type EventName, type EventPattern, type ExpressionOperator, type FetchResponseType, type Flow, type FlowDefinition, type FlowOptions, type InactiveBinding, type Input, type Mode, type Next, type Props, PubSub, type QueueOverflow, type RetryOptions, type RunOptions, type RunResult, type Runner, type RunnerErrorEvent, type RunnerOptions, type Runtime, type SlapError, type SlapErrorEvent, type SlapEvent, type StartResult, type Strategy, type TraceEntry, type TraceSink, type ValidationIssue, type ValidationResult, type VariableValue, type Variables, type WS, type WSOptions, type WSRetryOptions, type WSSocket, type WSStatus, catchError, createActionsRegistry, createConditionsRegistry, createFlow, createMemoryTraceSink, createPubSub, createWS, defineConfig, defineErrorReporter };
409
+ export { type Action, type ActionArgs, type ActionFail, type ActionResult, type ActionSkip, type ActionStop, type ActionSuccess, type ActionsRegistry, BUILTIN_ACTIONS, BUILTIN_ACTION_NAMES, BUILTIN_CONDITIONS, BUILTIN_CONDITION_NAMES, type BindingEventMap, type Bus, type BusBinding, type BusBindingKey, type BusBindings, type BusEmitOptions, type BusErrorEvent, type BusEvent, type BusOptions, type ConcurrencyMode, type ConcurrencyOptions, type ConditionExpression, type ConditionFn, type ConditionsRegistry, type Config, type DomBinding, type DomBindingKey, type DomBindings, type DomForm, type DomInput, type ErrorReporter, type ErrorReporterHandlers, type ErrorStage, type EventHandler, type EventMap, type EventName, type EventPattern, type ExpressionOperator, type FetchResponseType, type Flow, type FlowDefinition, type FlowOptions, type InactiveBinding, type Input, type Mode, type Next, type Props, PubSub, type QueueOverflow, type RetryOptions, type RunOptions, type RunResult, type Runner, type RunnerErrorEvent, type RunnerOptions, type Runtime, type SlapError, type SlapErrorEvent, type SlapEvent, type SocketEventTopic, type StartResult, type Strategy, type TraceEntry, type TraceSink, type ValidationIssue, type ValidationResult, type VariableValue, type Variables, type WS, type WSClient, type WSOptions, type WSRetryOptions, type WSSocket, type WSStatus, type WebSocketOptions, type WebSocketStatus, catchError, createFlow, createMemoryTraceSink, createPubSub, createWS, createWebSocket, defineConfig, defineErrorReporter };
package/dist/index.js CHANGED
@@ -1,9 +1,6 @@
1
1
  // src/helpers/config/defineConfig.ts
2
2
  var defineConfig = (config) => config;
3
3
 
4
- // src/helpers/trace/cloneData.ts
5
- var cloneData = (data) => ({ ...data });
6
-
7
4
  // src/helpers/trace/createMemoryTraceSink.ts
8
5
  var createMemoryTraceSink = () => {
9
6
  const items = [];
@@ -15,22 +12,9 @@ var createMemoryTraceSink = () => {
15
12
  };
16
13
  };
17
14
 
18
- // src/helpers/errors/slapError.ts
19
- var slapError = (code, message, extras = {}) => ({ code, message, ...extras });
20
-
21
15
  // src/helpers/errors/defineErrorReporter.ts
22
16
  var defineErrorReporter = (handlers) => typeof handlers === "function" ? handlers : handlers.report;
23
17
 
24
- // src/errors.ts
25
- var SyncAsyncError = class extends Error {
26
- slapError;
27
- constructor(error) {
28
- super(error.message);
29
- this.name = "SyncAsyncError";
30
- this.slapError = error;
31
- }
32
- };
33
-
34
18
  // src/helpers/ids/createId.ts
35
19
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
36
20
  var idLength = 12;
@@ -70,7 +54,7 @@ var serializeError = (error) => ({
70
54
  error: error instanceof Error ? error.message : String(error)
71
55
  });
72
56
 
73
- // src/pubSub.ts
57
+ // src/createPubSub.ts
74
58
  var createPubSub = (options = {}) => {
75
59
  const subscribers = /* @__PURE__ */ new Map();
76
60
  const wildcardSubscribers = /* @__PURE__ */ new Map();
@@ -168,6 +152,16 @@ var createPubSub = (options = {}) => {
168
152
  };
169
153
  var PubSub = createPubSub();
170
154
 
155
+ // src/helpers/errors/syncAsyncError.ts
156
+ var SyncAsyncError = class extends Error {
157
+ slapError;
158
+ constructor(error) {
159
+ super(error.message);
160
+ this.name = "SyncAsyncError";
161
+ this.slapError = error;
162
+ }
163
+ };
164
+
171
165
  // src/helpers/actions/coreDelay.ts
172
166
  var coreDelay = async ({
173
167
  props,
@@ -204,10 +198,7 @@ var coreEmit = ({
204
198
  };
205
199
 
206
200
  // src/helpers/actions/coreFail.ts
207
- var coreFail = ({
208
- props,
209
- runtime
210
- }) => runtime.fail(String(props.reason ?? "failed"), props.data);
201
+ var coreFail = ({ props, runtime }) => runtime.fail(String(props.reason ?? "failed"), props.data);
211
202
 
212
203
  // src/helpers/retry/getRetryDelay.ts
213
204
  var getRetryDelay = (attempt, options) => {
@@ -414,10 +405,7 @@ var corePatch = ({
414
405
  };
415
406
 
416
407
  // src/helpers/actions/coreSet.ts
417
- var coreSet = ({
418
- props,
419
- runtime
420
- }) => {
408
+ var coreSet = ({ props, runtime }) => {
421
409
  const path = props.path;
422
410
  if (typeof path === "string") {
423
411
  runtime.set(path, props.value);
@@ -438,13 +426,10 @@ var coreSetData = ({
438
426
  };
439
427
 
440
428
  // src/helpers/actions/coreStop.ts
441
- var coreStop = ({
442
- props,
443
- runtime
444
- }) => runtime.stop(String(props.reason ?? "stopped"));
429
+ var coreStop = ({ props, runtime }) => runtime.stop(String(props.reason ?? "stopped"));
445
430
 
446
- // src/registry/actions.ts
447
- var createActionsRegistry = () => /* @__PURE__ */ new Map([
431
+ // src/helpers/actions/index.ts
432
+ var BUILTIN_ACTIONS = [
448
433
  ["core.noop", coreNoop],
449
434
  ["core.stop", coreStop],
450
435
  ["core.fail", coreFail],
@@ -458,7 +443,8 @@ var createActionsRegistry = () => /* @__PURE__ */ new Map([
458
443
  ["core.emit", coreEmit],
459
444
  ["core.patch", corePatch],
460
445
  ["core.delay", coreDelay]
461
- ]);
446
+ ];
447
+ var BUILTIN_ACTION_NAMES = new Set(BUILTIN_ACTIONS.map(([name]) => name));
462
448
 
463
449
  // src/helpers/conditions/changedCondition.ts
464
450
  var changedCondition = (_args, current, previous) => !Object.is(current, previous);
@@ -552,8 +538,8 @@ var typeIsCondition = (_args, value, expected) => {
552
538
  return expected === "string" || expected === "number" || expected === "boolean" ? typeof value === expected : false;
553
539
  };
554
540
 
555
- // src/registry/conditions.ts
556
- var createConditionsRegistry = () => /* @__PURE__ */ new Map([
541
+ // src/helpers/conditions/index.ts
542
+ var BUILTIN_CONDITIONS = [
557
543
  ["eq", eqCondition],
558
544
  ["neq", neqCondition],
559
545
  ["gt", gtCondition],
@@ -570,7 +556,8 @@ var createConditionsRegistry = () => /* @__PURE__ */ new Map([
570
556
  ["typeIs", typeIsCondition],
571
557
  ["changed", changedCondition],
572
558
  ["cooldownReady", cooldownReadyCondition]
573
- ]);
559
+ ];
560
+ var BUILTIN_CONDITION_NAMES = new Set(BUILTIN_CONDITIONS.map(([name]) => name));
574
561
 
575
562
  // src/helpers/runner/applyResult.ts
576
563
  var applyResult = (result, state, mergeData) => {
@@ -584,6 +571,9 @@ var applyResult = (result, state, mergeData) => {
584
571
  state.events.push(...result.events);
585
572
  };
586
573
 
574
+ // src/helpers/errors/slapError.ts
575
+ var slapError = (code, message, extras = {}) => ({ code, message, ...extras });
576
+
587
577
  // src/helpers/path/resolveValue.ts
588
578
  import { pick as pick3 } from "objwalk";
589
579
 
@@ -1022,7 +1012,7 @@ var executeNext = (item, depth, state, environment) => {
1022
1012
  const id = typeof item === "string" ? item : item.strategy;
1023
1013
  if (typeof item !== "string" && item.when) {
1024
1014
  const runtime = createRuntime(state);
1025
- const condition = evaluateCondition(item.when, environment.conditionsRegistry, { ...state, runtime, strategy: id });
1015
+ const condition = evaluateCondition(item.when, environment.registry.conditions, { ...state, runtime, strategy: id });
1026
1016
  if (!condition.ok) {
1027
1017
  return { status: "failed", error: condition.error, patches: [], events: [] };
1028
1018
  }
@@ -1274,6 +1264,9 @@ var normalizeActionResult = (raw) => {
1274
1264
  };
1275
1265
  };
1276
1266
 
1267
+ // src/helpers/trace/cloneData.ts
1268
+ var cloneData = (data) => ({ ...data });
1269
+
1277
1270
  // src/helpers/runner/pushTrace.ts
1278
1271
  var pushTrace = (state, step, depth, strategyId, strategy, status, props, dataBefore, startedAt, reason) => {
1279
1272
  state.traceSink?.push({
@@ -1456,7 +1449,7 @@ var executeStrategy = (id, extraProps, depth, state, environment) => {
1456
1449
  events: []
1457
1450
  };
1458
1451
  }
1459
- const action = environment.actionsRegistry.get(strategy.fn);
1452
+ const action = environment.registry.actions.get(strategy.fn);
1460
1453
  if (!action) {
1461
1454
  return {
1462
1455
  status: "failed",
@@ -1511,7 +1504,7 @@ var executeStrategy = (id, extraProps, depth, state, environment) => {
1511
1504
  const dataBefore = cloneData(state.data);
1512
1505
  const traceStep = state.stepCounter.current + 1;
1513
1506
  const startedAt = Date.now();
1514
- const condition = evaluateCondition(strategy.when, environment.conditionsRegistry, {
1507
+ const condition = evaluateCondition(strategy.when, environment.registry.conditions, {
1515
1508
  ...state,
1516
1509
  runtime,
1517
1510
  strategy: id
@@ -1825,7 +1818,12 @@ var validateCondition = (expression, strategy, path, conditionsRegistry, errors)
1825
1818
  const [operator, ...args] = expression;
1826
1819
  if (operator === "guard") {
1827
1820
  if (args.length !== 1 || typeof args[0] !== "string") {
1828
- errors.push({ code: "CONDITION_INVALID", message: "Guard reference must be a single string name", strategy, path });
1821
+ errors.push({
1822
+ code: "CONDITION_INVALID",
1823
+ message: "Guard reference must be a single string name",
1824
+ strategy,
1825
+ path
1826
+ });
1829
1827
  }
1830
1828
  return;
1831
1829
  }
@@ -1878,7 +1876,9 @@ var validateNextList = (config, list, path, strategy, conditionsRegistry, errors
1878
1876
  var isGuardRef = (expression) => Array.isArray(expression) && expression.length === 2 && expression[0] === "guard" && typeof expression[1] === "string";
1879
1877
  var resolveRef = (config, name, visiting, path) => {
1880
1878
  if (visiting.includes(name)) {
1881
- return { issue: { code: "GUARD_CYCLE", message: `Guard "${name}" is part of a reference cycle`, guard: name, path } };
1879
+ return {
1880
+ issue: { code: "GUARD_CYCLE", message: `Guard "${name}" is part of a reference cycle`, guard: name, path }
1881
+ };
1882
1882
  }
1883
1883
  const guard = config.guards?.[name];
1884
1884
  if (guard === void 0) {
@@ -2101,10 +2101,10 @@ var createRunCancellation = (source) => {
2101
2101
  };
2102
2102
  };
2103
2103
 
2104
- // src/helpers/runner/createRunner.ts
2104
+ // src/createRunner.ts
2105
2105
  var createRunner = (options = {}) => {
2106
- const actionsRegistry = createActionsRegistry();
2107
- const conditionsRegistry = createConditionsRegistry();
2106
+ const actionsRegistry = new Map(BUILTIN_ACTIONS);
2107
+ const conditionsRegistry = new Map(BUILTIN_CONDITIONS);
2108
2108
  const configRef = {};
2109
2109
  const timeout = options.timeout ?? options.timeoutMs;
2110
2110
  const runnerOptions = timeout === void 0 ? options : { ...options, timeout };
@@ -2114,19 +2114,24 @@ var createRunner = (options = {}) => {
2114
2114
  console.warn("timeoutMs is deprecated; use timeout. It will be removed in a future major release.");
2115
2115
  }
2116
2116
  const environment = {
2117
- actionsRegistry,
2118
- conditionsRegistry,
2117
+ registry: { actions: actionsRegistry, conditions: conditionsRegistry },
2119
2118
  configRef,
2120
2119
  options: runnerOptions,
2121
2120
  mergeData
2122
2121
  };
2123
2122
  const registerAction = (name, action) => {
2123
+ if (BUILTIN_ACTION_NAMES.has(name)) {
2124
+ throw new Error(`Cannot override built-in action "${name}"`);
2125
+ }
2124
2126
  actionsRegistry.set(name, action);
2125
2127
  };
2126
2128
  const registerActions = (items) => {
2127
2129
  Object.entries(items).forEach(([name, action]) => registerAction(name, action));
2128
2130
  };
2129
2131
  const registerCondition = (name, condition) => {
2132
+ if (BUILTIN_CONDITION_NAMES.has(name)) {
2133
+ throw new Error(`Cannot override built-in condition "${name}"`);
2134
+ }
2130
2135
  conditionsRegistry.set(name, condition);
2131
2136
  };
2132
2137
  const registerConditions = (items) => {
@@ -2223,7 +2228,7 @@ var parseDomBinding = (binding, prefix) => {
2223
2228
  return separator <= 0 || separator === source.length - 1 ? void 0 : { selector: source.slice(0, separator), eventType: source.slice(separator + 1) };
2224
2229
  };
2225
2230
 
2226
- // src/flow.ts
2231
+ // src/createFlow.ts
2227
2232
  var busBindingPrefix = "[bus] ";
2228
2233
  var domBindingPrefix = "[dom] ";
2229
2234
  var defaultMaxQueueSize = 50;
@@ -2464,10 +2469,11 @@ var createFlow = (definition, options) => {
2464
2469
  return { runner, start, stop };
2465
2470
  };
2466
2471
 
2467
- // src/ws.ts
2472
+ // src/createWS.ts
2468
2473
  var openState = 1;
2469
2474
  var maxSeenEvents = 1e3;
2470
2475
  var createWS = (options) => {
2476
+ console.warn("[slapflow] createWS is deprecated and will be removed soon. Use createWebSocket instead.");
2471
2477
  const inboundTopics = new Set(options.inboundTopics ?? []);
2472
2478
  const outboundTopics = new Set(options.outboundTopics ?? []);
2473
2479
  const seenEventIds = /* @__PURE__ */ new Set();
@@ -2631,7 +2637,117 @@ var createWS = (options) => {
2631
2637
  return { start, stop, reconnect, status: () => currentStatus };
2632
2638
  };
2633
2639
 
2634
- // src/catchError.ts
2640
+ // src/createWebSocket.ts
2641
+ var createWebSocket = (options) => {
2642
+ let socket;
2643
+ let retryTimer;
2644
+ let retryAttempt = 0;
2645
+ let started = false;
2646
+ let currentStatus = "idle";
2647
+ const emitSocketEvent = (topic, payload) => {
2648
+ options.bus.dispatch({
2649
+ id: createId(),
2650
+ topic,
2651
+ occurredAt: Date.now(),
2652
+ ...options.origin ? { origin: options.origin } : {},
2653
+ parsed: payload,
2654
+ serialized: JSON.stringify(payload)
2655
+ });
2656
+ };
2657
+ const scheduleRetry = () => {
2658
+ const delay = getRetryDelay(retryAttempt, options.reconnect ?? {});
2659
+ const attempt = retryAttempt + 1;
2660
+ currentStatus = "reconnecting";
2661
+ retryAttempt = attempt;
2662
+ retryTimer = setTimeout(() => {
2663
+ retryTimer = void 0;
2664
+ connect();
2665
+ }, delay);
2666
+ };
2667
+ const connect = () => {
2668
+ if (!started || socket) {
2669
+ return;
2670
+ }
2671
+ currentStatus = "connecting";
2672
+ try {
2673
+ const current = new WebSocket(options.url, options.protocols ?? []);
2674
+ socket = current;
2675
+ current.addEventListener("open", () => {
2676
+ if (socket === current) {
2677
+ retryAttempt = 0;
2678
+ currentStatus = "connected";
2679
+ emitSocketEvent("open", { url: options.url });
2680
+ }
2681
+ });
2682
+ current.addEventListener("message", (event) => {
2683
+ if (socket !== current) {
2684
+ return;
2685
+ }
2686
+ let parsed = event.data;
2687
+ if (typeof parsed === "string") {
2688
+ try {
2689
+ parsed = JSON.parse(parsed);
2690
+ } catch {
2691
+ parsed = event.data;
2692
+ }
2693
+ }
2694
+ emitSocketEvent("message", parsed);
2695
+ });
2696
+ current.addEventListener("close", (event) => {
2697
+ if (socket !== current) {
2698
+ return;
2699
+ }
2700
+ socket = void 0;
2701
+ emitSocketEvent("close", { code: event.code, reason: event.reason });
2702
+ if (started) {
2703
+ scheduleRetry();
2704
+ }
2705
+ });
2706
+ current.addEventListener("error", (event) => {
2707
+ emitSocketEvent("error", { error: event });
2708
+ });
2709
+ } catch (error) {
2710
+ socket = void 0;
2711
+ emitSocketEvent("error", { error });
2712
+ if (started) {
2713
+ scheduleRetry();
2714
+ }
2715
+ }
2716
+ };
2717
+ const start = () => {
2718
+ if (!started) {
2719
+ started = true;
2720
+ connect();
2721
+ }
2722
+ };
2723
+ const stop = () => {
2724
+ started = false;
2725
+ if (retryTimer) {
2726
+ clearTimeout(retryTimer);
2727
+ }
2728
+ retryTimer = void 0;
2729
+ const current = socket;
2730
+ socket = void 0;
2731
+ current?.close();
2732
+ currentStatus = "stopped";
2733
+ };
2734
+ const reconnect = () => {
2735
+ if (!started) {
2736
+ return;
2737
+ }
2738
+ if (retryTimer) {
2739
+ clearTimeout(retryTimer);
2740
+ retryTimer = void 0;
2741
+ }
2742
+ const current = socket;
2743
+ socket = void 0;
2744
+ current?.close();
2745
+ connect();
2746
+ };
2747
+ return { start, stop, reconnect, status: () => currentStatus };
2748
+ };
2749
+
2750
+ // src/helpers/catchError.ts
2635
2751
  var catchError = (callback) => new Promise((resolve, reject) => {
2636
2752
  try {
2637
2753
  resolve(callback());
@@ -2640,14 +2756,17 @@ var catchError = (callback) => new Promise((resolve, reject) => {
2640
2756
  }
2641
2757
  });
2642
2758
  export {
2759
+ BUILTIN_ACTIONS,
2760
+ BUILTIN_ACTION_NAMES,
2761
+ BUILTIN_CONDITIONS,
2762
+ BUILTIN_CONDITION_NAMES,
2643
2763
  PubSub,
2644
2764
  catchError,
2645
- createActionsRegistry,
2646
- createConditionsRegistry,
2647
2765
  createFlow,
2648
2766
  createMemoryTraceSink,
2649
2767
  createPubSub,
2650
2768
  createWS,
2769
+ createWebSocket,
2651
2770
  defineConfig,
2652
2771
  defineErrorReporter
2653
2772
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slapflow",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Chain actions behavior runtime",
5
5
  "license": "ISC",
6
6
  "author": "Sergey Khalilov",
@@ -57,7 +57,9 @@
57
57
  "test": "npm run typecheck && vitest run",
58
58
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
59
59
  "pack:check": "npm pack --dry-run",
60
- "prepublishOnly": "npm test && npm run build"
60
+ "prepublishOnly": "npm test && npm run build",
61
+ "format": "prettier --write .",
62
+ "check": "prettier --check ."
61
63
  },
62
64
  "devDependencies": {
63
65
  "@types/node": "^26.1.0",