@hotmeshio/long-tail-dbos 0.0.0-stage → 0.1.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/LICENSE +221 -0
- package/README.md +226 -2
- package/build/conditional.d.ts +11 -0
- package/build/conditional.js +41 -0
- package/build/config.d.ts +6 -0
- package/build/config.js +29 -0
- package/build/constants.d.ts +33 -0
- package/build/constants.js +46 -0
- package/build/duration.d.ts +2 -0
- package/build/duration.js +20 -0
- package/build/index.d.ts +6 -0
- package/build/index.js +15 -0
- package/build/lt-client.d.ts +38 -0
- package/build/lt-client.js +78 -0
- package/build/provider/index.d.ts +22 -0
- package/build/provider/index.js +96 -0
- package/build/provider/list.d.ts +20 -0
- package/build/provider/list.js +47 -0
- package/build/provider/status.d.ts +6 -0
- package/build/provider/status.js +38 -0
- package/build/provider/steps-map.d.ts +22 -0
- package/build/provider/steps-map.js +128 -0
- package/build/start.d.ts +10 -0
- package/build/start.js +72 -0
- package/build/steps.d.ts +7 -0
- package/build/steps.js +50 -0
- package/build/types.d.ts +39 -0
- package/build/types.js +2 -0
- package/package.json +65 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Plain-language summary (not part of the license)
|
|
2
|
+
|
|
3
|
+
Long Tail for DBOS is source available under the Apache License 2.0 with
|
|
4
|
+
the Commons Clause condition below. In short:
|
|
5
|
+
|
|
6
|
+
- You may use, copy, modify and run it, in production and inside your
|
|
7
|
+
organization, and self-host it for your own work.
|
|
8
|
+
- You may build and sell your own products that use it, as long as their
|
|
9
|
+
value does not derive entirely or substantially from Long Tail for DBOS.
|
|
10
|
+
- You may not sell Long Tail for DBOS itself: no product, hosted service,
|
|
11
|
+
or paid hosting, consulting or support whose value derives entirely or
|
|
12
|
+
substantially from it. Only HotMeshIO offers Long Tail for DBOS
|
|
13
|
+
commercially.
|
|
14
|
+
|
|
15
|
+
This summary is a convenience. It does not change the license below; where
|
|
16
|
+
the two differ, the license governs.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
"Commons Clause" License Condition v1.0
|
|
21
|
+
|
|
22
|
+
The Software is provided to you by the Licensor under the License, as
|
|
23
|
+
defined below, subject to the following condition.
|
|
24
|
+
|
|
25
|
+
Without limiting other conditions in the License, the grant of rights
|
|
26
|
+
under the License will not include, and the License does not grant to
|
|
27
|
+
you, the right to Sell the Software.
|
|
28
|
+
|
|
29
|
+
For purposes of the foregoing, "Sell" means practicing any or all of
|
|
30
|
+
the rights granted to you under the License to provide to third parties,
|
|
31
|
+
for a fee or other consideration (including without limitation fees for
|
|
32
|
+
hosting or consulting/support services related to the Software), a
|
|
33
|
+
product or service whose value derives, entirely or substantially, from
|
|
34
|
+
the functionality of the Software. Any license notice or attribution
|
|
35
|
+
required by the License must also include this Commons Clause License
|
|
36
|
+
Condition notice.
|
|
37
|
+
|
|
38
|
+
Software: HotMesh Long Tail for DBOS
|
|
39
|
+
|
|
40
|
+
License: Apache 2.0
|
|
41
|
+
|
|
42
|
+
Licensor: HotMeshIO
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
Apache License
|
|
47
|
+
Version 2.0, January 2004
|
|
48
|
+
http://www.apache.org/licenses/
|
|
49
|
+
|
|
50
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
51
|
+
|
|
52
|
+
1. Definitions.
|
|
53
|
+
|
|
54
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
55
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
56
|
+
|
|
57
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
58
|
+
the copyright owner that is granting the License.
|
|
59
|
+
|
|
60
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
61
|
+
other entities that control, are controlled by, or are under common
|
|
62
|
+
control with that entity. For the purposes of this definition,
|
|
63
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
64
|
+
direction or management of such entity, whether by contract or
|
|
65
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
66
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
67
|
+
|
|
68
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
69
|
+
exercising permissions granted by this License.
|
|
70
|
+
|
|
71
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
72
|
+
including but not limited to software source code, documentation
|
|
73
|
+
source, and configuration files.
|
|
74
|
+
|
|
75
|
+
"Object" form shall mean any form resulting from mechanical
|
|
76
|
+
transformation or translation of a Source form, including but
|
|
77
|
+
not limited to compiled object code, generated documentation,
|
|
78
|
+
and conversions to other media types.
|
|
79
|
+
|
|
80
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
81
|
+
Object form, made available under the License, as indicated by a
|
|
82
|
+
copyright notice that is included in or attached to the work
|
|
83
|
+
(an example is provided in the Appendix below).
|
|
84
|
+
|
|
85
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
86
|
+
form, that is based on (or derived from) the Work and for which the
|
|
87
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
88
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
89
|
+
of this License, Derivative Works shall not include works that remain
|
|
90
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
91
|
+
the Work and Derivative Works thereof.
|
|
92
|
+
|
|
93
|
+
"Contribution" shall mean any work of authorship, including
|
|
94
|
+
the original version of the Work and any modifications or additions
|
|
95
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
96
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
97
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
98
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
99
|
+
means any form of electronic, verbal, or written communication sent
|
|
100
|
+
to the Licensor or its representatives, including but not limited to
|
|
101
|
+
communication on electronic mailing lists, source code control systems,
|
|
102
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
103
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
104
|
+
excluding communication that is conspicuously marked or otherwise
|
|
105
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
106
|
+
|
|
107
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
108
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
109
|
+
subsequently incorporated within the Work.
|
|
110
|
+
|
|
111
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
112
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
113
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
114
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
115
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
116
|
+
Work and such Derivative Works in Source or Object form.
|
|
117
|
+
|
|
118
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
119
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
120
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
121
|
+
(except as stated in this section) patent license to make, have made,
|
|
122
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
123
|
+
where such license applies only to those patent claims licensable
|
|
124
|
+
by such Contributor that are necessarily infringed by their
|
|
125
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
126
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
127
|
+
institute patent litigation against any entity (including a
|
|
128
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
129
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
130
|
+
or contributory patent infringement, then any patent licenses
|
|
131
|
+
granted to You under this License for that Work shall terminate
|
|
132
|
+
as of the date such litigation is filed.
|
|
133
|
+
|
|
134
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
135
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
136
|
+
modifications, and in Source or Object form, provided that You
|
|
137
|
+
meet the following conditions:
|
|
138
|
+
|
|
139
|
+
(a) You must give any other recipients of the Work or
|
|
140
|
+
Derivative Works a copy of this License; and
|
|
141
|
+
|
|
142
|
+
(b) You must cause any modified files to carry prominent notices
|
|
143
|
+
stating that You changed the files; and
|
|
144
|
+
|
|
145
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
146
|
+
that You distribute, all copyright, patent, trademark, and
|
|
147
|
+
attribution notices from the Source form of the Work,
|
|
148
|
+
excluding those notices that do not pertain to any part of
|
|
149
|
+
the Derivative Works; and
|
|
150
|
+
|
|
151
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
152
|
+
distribution, then any Derivative Works that You distribute must
|
|
153
|
+
include a readable copy of the attribution notices contained
|
|
154
|
+
within such NOTICE file, excluding those notices that do not
|
|
155
|
+
pertain to any part of the Derivative Works, in at least one
|
|
156
|
+
of the following places: within a NOTICE text file distributed
|
|
157
|
+
as part of the Derivative Works; within the Source form or
|
|
158
|
+
documentation, if provided along with the Derivative Works; or,
|
|
159
|
+
within a display generated by the Derivative Works, if and
|
|
160
|
+
wherever such third-party notices normally appear. The contents
|
|
161
|
+
of the NOTICE file are for informational purposes only and
|
|
162
|
+
do not modify the License. You may add Your own attribution
|
|
163
|
+
notices within Derivative Works that You distribute, alongside
|
|
164
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
165
|
+
that such additional attribution notices cannot be construed
|
|
166
|
+
as modifying the License.
|
|
167
|
+
|
|
168
|
+
You may add Your own copyright statement to Your modifications and
|
|
169
|
+
may provide additional or different license terms and conditions
|
|
170
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
171
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
172
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
173
|
+
the conditions stated in this License.
|
|
174
|
+
|
|
175
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
176
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
177
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
178
|
+
this License, without any additional terms or conditions.
|
|
179
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
180
|
+
the terms of any separate license agreement you may have executed
|
|
181
|
+
with Licensor regarding such Contributions.
|
|
182
|
+
|
|
183
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
184
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
185
|
+
except as required for reasonable and customary use in describing the
|
|
186
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
187
|
+
|
|
188
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
189
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
190
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
191
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
192
|
+
implied, including, without limitation, any warranties or conditions
|
|
193
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
194
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
195
|
+
appropriateness of using or redistributing the Work and assume any
|
|
196
|
+
risks associated with Your exercise of permissions under this License.
|
|
197
|
+
|
|
198
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
199
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
200
|
+
unless required by applicable law (such as deliberate and grossly
|
|
201
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
202
|
+
liable to You for damages, including any direct, indirect, special,
|
|
203
|
+
incidental, or consequential damages of any character arising as a
|
|
204
|
+
result of this License or out of the use or inability to use the
|
|
205
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
206
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
207
|
+
other commercial damages or losses), even if such Contributor
|
|
208
|
+
has been advised of the possibility of such damages.
|
|
209
|
+
|
|
210
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
211
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
212
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
213
|
+
or other liability obligations and/or rights consistent with this
|
|
214
|
+
License. However, in accepting such obligations, You may act only
|
|
215
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
216
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
217
|
+
defend, and hold each Contributor harmless for any liability
|
|
218
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
219
|
+
of your accepting any such warranty or additional liability.
|
|
220
|
+
|
|
221
|
+
END OF TERMS AND CONDITIONS
|
package/README.md
CHANGED
|
@@ -1,3 +1,227 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Long Tail for DBOS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Human steps for DBOS workflows, on your own Postgres
|
|
4
|
+
|
|
5
|
+
**One call, `conditional()`, gives a DBOS workflow a full human-in-the-loop desk: role queues, claims, forms, SLAs and a dashboard. It runs entirely on-prem: Postgres, your app and one Long Tail container. No cloud service, no extra server to operate.**
|
|
6
|
+
|
|
7
|
+
DBOS is a library over Postgres. Long Tail is a container over Postgres. Together they run where your data already lives, with no connection to anything outside your network. A DBOS workflow that needs a person to approve, correct or decide something calls `conditional()`, parks, and gets the answer back as a typed value. Everything between those two moments (who sees the work, who claims it, which form they fill in, what happens when nobody answers in time) is Long Tail's job.
|
|
8
|
+
|
|
9
|
+
### The problem
|
|
10
|
+
|
|
11
|
+
Durable workflows are good at the parts machines do. The moment a step needs a person, every team builds the same things again: a table of pending tasks, a way to route them to the right people, a UI to work them, a timeout, an audit trail, and the plumbing to wake the workflow when someone answers. That code is rarely the product, and it is rarely done well.
|
|
12
|
+
|
|
13
|
+
### The solution
|
|
14
|
+
|
|
15
|
+
Take one small step sideways. Keep DBOS exactly as it is, and call `conditional()` where a person belongs:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { DBOS } from '@dbos-inc/dbos-sdk';
|
|
19
|
+
import { conditional } from '@hotmeshio/long-tail-dbos';
|
|
20
|
+
|
|
21
|
+
async function reviewOrderFn(orderId: string) {
|
|
22
|
+
const { risk } = await DBOS.runStep(() => scoreOrder(orderId), { name: 'score' });
|
|
23
|
+
|
|
24
|
+
const decision = await conditional<{ approved: boolean; notes?: string }>(`review-${orderId}`, {
|
|
25
|
+
role: 'reviewer',
|
|
26
|
+
type: 'order-review',
|
|
27
|
+
description: `Review order ${orderId}`,
|
|
28
|
+
metadata: { orderId },
|
|
29
|
+
data: { risk },
|
|
30
|
+
timeout: '48h',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!decision) return { orderId, status: 'expired-or-cancelled' };
|
|
34
|
+
if (decision.approved) await DBOS.runStep(() => ship(orderId), { name: 'ship' });
|
|
35
|
+
return { orderId, status: decision.approved ? 'shipped' : 'rejected' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const reviewOrder = DBOS.registerWorkflow(reviewOrderFn, { name: 'reviewOrder' });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The workflow parks in `DBOS.recv`. An escalation appears in the `reviewer` queue in the Long Tail dashboard. A reviewer claims it, fills in the role's form, and submits. Long Tail delivers one terminal outcome back to the workflow, which carries on.
|
|
42
|
+
|
|
43
|
+
### `conditional()` at a glance
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const answer = await conditional<T>(key, ask);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `key` names the wait within the workflow. Opening the same key again returns the same escalation, so a retried step never creates a second one.
|
|
50
|
+
- `ask` describes the escalation: `role` (the queue), `type`, `description`, `priority` (1 to 4), `metadata` (searchable facets), `data` (shown to the resolver) and `timeout` (the SLA, such as `'48h'`).
|
|
51
|
+
- `answer` is the resolver's payload, typed as `T`, or `null` when the escalation expired or was cancelled.
|
|
52
|
+
|
|
53
|
+
The same call works in HotMesh and Temporal workflows. Long Tail's documentation is at [hotmesh.io](https://hotmesh.io/docs/start/quickstart); [conditional()](https://hotmesh.io/docs/build/conditional) covers roles, forms and facets in depth.
|
|
54
|
+
|
|
55
|
+
## Getting started in five minutes
|
|
56
|
+
|
|
57
|
+
You need Docker.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
docker compose up -d --build # Postgres, Long Tail, and the example DBOS app
|
|
61
|
+
curl -X POST localhost:4000/orders/42 # starts reviewOrder for order 42
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Open http://localhost:3400, sign in as `superadmin` / `l0ngt@1l` (set `LT_ADMIN_PASSWORD` in `.env` to change it), open the **reviewer** queue, claim the review for order 42 and resolve it. The app logs the result.
|
|
65
|
+
|
|
66
|
+
Try the restart story while the review is waiting:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
curl -X POST localhost:4000/orders/43
|
|
70
|
+
docker compose restart review-order # the app goes away mid-wait
|
|
71
|
+
# resolve order 43 in the dashboard; the restarted app recovers the workflow and it completes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Everything in `docker-compose.yml` is local: one Postgres database, `longtail`, holding Long Tail's tables and DBOS's system tables (in the `dbos` schema), the Long Tail container (API, dashboard and delivery relay) and the example app. Nothing calls out to the internet.
|
|
75
|
+
|
|
76
|
+
| Port | Service | Override |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| 3400 | Long Tail dashboard and API | `LT_PORT` |
|
|
79
|
+
| 4000 | The example DBOS app | `APP_PORT` |
|
|
80
|
+
| 5425 | Postgres (`longtail`) | `PG_PORT` |
|
|
81
|
+
|
|
82
|
+
The example app mints its own Long Tail bot key at startup by signing in as the admin. That is a local convenience; in production, set `LT_API_KEY`.
|
|
83
|
+
|
|
84
|
+
### In your own app
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm install @hotmeshio/long-tail-dbos @dbos-inc/dbos-sdk
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { DBOS } from '@dbos-inc/dbos-sdk';
|
|
92
|
+
import { configureLongTail } from '@hotmeshio/long-tail-dbos'; // registers the lt.open and lt.close steps
|
|
93
|
+
|
|
94
|
+
configureLongTail({ url: process.env.LT_URL!, apiKey: process.env.LT_API_KEY! });
|
|
95
|
+
DBOS.setConfig({ name: 'orders', systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL });
|
|
96
|
+
await DBOS.launch();
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Import the package before `DBOS.launch()`. DBOS requires steps to be registered first, and the package says so clearly if the order is wrong.
|
|
100
|
+
- `LT_API_KEY` is a Long Tail bot account key (create the bot and its key once in the dashboard) whose roles allow creating escalations in the roles your workflows use.
|
|
101
|
+
|
|
102
|
+
Run Long Tail with the provider, either from this repo's image (`node build/start.js`) or in your own code. The image reads its configuration from the environment:
|
|
103
|
+
|
|
104
|
+
| Variable | Meaning |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `DBOS_SYSTEM_DATABASE_URL` | The DBOS system database your app uses. Long Tail reads it to deliver outcomes and to show the timeline. Defaults to `DATABASE_URL`: one database, with DBOS's tables in the `dbos` schema. |
|
|
107
|
+
| `DATABASE_URL` or `POSTGRES_HOST/PORT/USER/PASSWORD/DB` | Long Tail's database. |
|
|
108
|
+
| `PORT`, `JWT_SECRET` | Long Tail's HTTP port and token secret. |
|
|
109
|
+
| `LT_ADMIN_USER`, `LT_ADMIN_PASSWORD`, `LT_ADMIN_EMAIL` | The seeded admin. |
|
|
110
|
+
| `LT_ROLES` | Comma-separated roles to create, such as `reviewer,finance`. |
|
|
111
|
+
| `LT_START_CONFIG` | Optional JSON file with further `start()` settings, such as role titles or SSO. |
|
|
112
|
+
|
|
113
|
+
In your own code:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { start } from '@hotmeshio/long-tail';
|
|
117
|
+
import { dbosProvider } from '@hotmeshio/long-tail-dbos/provider';
|
|
118
|
+
|
|
119
|
+
await start({
|
|
120
|
+
database: { connectionString: process.env.DATABASE_URL },
|
|
121
|
+
durable: { provider: await dbosProvider({ systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL! }) },
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## FAQ
|
|
126
|
+
|
|
127
|
+
### What does DBOS still own, and what does Long Tail own?
|
|
128
|
+
|
|
129
|
+
| DBOS | Long Tail |
|
|
130
|
+
|---|---|
|
|
131
|
+
| Checkpoints, step retries, recovery, executor and version handling | The escalation row, role routing, RBAC, claims and reassignment |
|
|
132
|
+
| Time inside the workflow, and the `recv` that parks it | Forms, schema enforcement, facets, priority, the dashboard and MCP tools |
|
|
133
|
+
| Your workflow and step code | SLA expiry, cancellation by a person, and delivery of the outcome |
|
|
134
|
+
|
|
135
|
+
The two meet at one call. `conditional()` opens a wait and receives exactly one terminal outcome: `resolved` (with the resolver's payload), `expired` or `cancelled`. It returns the payload, or `null` for the other two.
|
|
136
|
+
|
|
137
|
+
### How is the outcome delivered?
|
|
138
|
+
|
|
139
|
+
When an escalation reaches a terminal state, by any path (dashboard, API, MCP tool, bulk action, scan rule, expiry), Long Tail's relay delivers it:
|
|
140
|
+
|
|
141
|
+
1. It checks that the workflow is still waiting (`DBOSClient.getWorkflow`). A finished or missing workflow is recorded as gone and never sent; DBOS would otherwise store the message without error and nobody would read it.
|
|
142
|
+
2. It calls `DBOSClient.send(workflowId, outcome, waitKey, escalationId)`. The escalation id is the DBOS idempotency key, so a retried delivery stores one message.
|
|
143
|
+
3. It records the delivery. Failed sends retry with capped backoff, and a lease keeps two Long Tail replicas from delivering the same wait.
|
|
144
|
+
|
|
145
|
+
If the app is down when someone resolves, the message waits in the DBOS system database. When the app restarts and recovers the workflow, `recv` reads it.
|
|
146
|
+
|
|
147
|
+
### Why must keys be unique per workflow?
|
|
148
|
+
|
|
149
|
+
Long Tail opens a wait once per `(workflowId, key)`, so a retried `lt.open` step never creates a second escalation. A loop that asks again uses a new key per round: `review-${orderId}-${round}`. A forked workflow has a new id, so it opens new waits. A `rewindWorkflow` that re-runs the wait reopens it, and Long Tail delivers the outcome again.
|
|
150
|
+
|
|
151
|
+
### How do SLAs, expiry and the `recv` deadline fit together?
|
|
152
|
+
|
|
153
|
+
- `timeout` (for example `'48h'`) becomes the escalation's expiry. Long Tail expires the row on time and delivers `expired`; `conditional()` returns `null`.
|
|
154
|
+
- DBOS gives `recv` a default timeout of 60 seconds, so `conditional()` always sets its own: the SLA plus ten minutes of grace, so Long Tail's expiry arrives first.
|
|
155
|
+
- With no SLA, the backstop is `maxWaitSeconds` (default 365 days).
|
|
156
|
+
- If the backstop passes before any outcome, the `lt.close` step cancels the escalation and `conditional()` returns `null`.
|
|
157
|
+
|
|
158
|
+
### What does the dashboard show for a DBOS workflow?
|
|
159
|
+
|
|
160
|
+
All of the escalation work surfaces: queues, claims, forms, history, analytics, roles and users.
|
|
161
|
+
|
|
162
|
+
**Executions** (under Workflows in the sidebar) lists DBOS workflows, newest first. It filters by workflow id prefix, workflow type and state. Running covers `PENDING`, `ENQUEUED`, `DELAYED` and `SUSPENDED`; failed covers `ERROR`, `CANCELLED` and `MAX_RECOVERY_ATTEMPTS_EXCEEDED`. The rows come from `DBOSClient.listWorkflows`. The total comes from a count over the `workflow_status` table in the DBOS system database, with the same filters, because the client lists but does not count.
|
|
163
|
+
|
|
164
|
+
The workflow execution page builds its timeline from DBOS's own records (`getWorkflow` and `listWorkflowSteps`):
|
|
165
|
+
|
|
166
|
+
- steps as activities;
|
|
167
|
+
- the wait as a span from `lt.open` to the `recv` that read the outcome;
|
|
168
|
+
- timers, child workflows and failures.
|
|
169
|
+
|
|
170
|
+
DBOS records step outputs but not step inputs, so the timeline shows outputs and the workflow's input. The wait ends when the workflow read the outcome; if the app was down when someone resolved, the escalation shows the resolve time.
|
|
171
|
+
|
|
172
|
+
Pages that only make sense on HotMesh (YAML pipelines, stream tables, control plane, triage and AI query) are hidden. Their API routes answer 501 with a message naming the engine.
|
|
173
|
+
|
|
174
|
+
### Running on-prem without Conductor
|
|
175
|
+
|
|
176
|
+
This package targets self-hosted DBOS: the open source library and your Postgres. Recovery works without any hosted service, provided each replica keeps a stable identity. The integration tests verify each point below.
|
|
177
|
+
|
|
178
|
+
- **Stable executor IDs per replica.** DBOS tags each workflow with the executor that runs it (`DBOS__VMID`, or `executorID` in `DBOS.setConfig`; the default is `local`). On launch, a process recovers pending workflows tagged with its own executor ID. Give every replica a fixed ID, such as the compose service name or a StatefulSet ordinal. A restarted replica then picks up its own parked waits.
|
|
179
|
+
- **A pinned application version, plus `DBOS.patch` for code changes.** DBOS recovers a workflow only on a process running the same application version (`DBOS__APPVERSION`, or `applicationVersion`). The default is a hash of your code, so an ordinary redeploy would leave earlier waits parked. Pin the version. When you change a workflow's code path, gate the new branch with `DBOS.patch('name')`: waits parked under the old code replay the old path, and new runs take the new one.
|
|
180
|
+
- **Retiring a replica.** Its workflows stay tagged with its executor ID. Recover them from a running replica:
|
|
181
|
+
- Find them with `DBOS.listWorkflows({ status: 'PENDING', executorId: 'replica-a' })`.
|
|
182
|
+
- Adopt each one with `DBOS.resumeWorkflow(id)`.
|
|
183
|
+
|
|
184
|
+
DBOS also documents an admin recovery endpoint, `POST /dbos-workflow-recovery`. The TypeScript 5.5 package this repo tests against does not include it, so `resumeWorkflow` is the path shown here. Only resume workflows from a replica that is really gone.
|
|
185
|
+
- **Memory per parked wait.** A workflow parked in `recv` is an in-memory promise in the app process. DBOS listens for messages with Postgres LISTEN/NOTIFY and also polls periodically as a fallback. A restart replays each pending workflow from its checkpoints until it parks again. Size the app process for the number of open escalations it holds.
|
|
186
|
+
|
|
187
|
+
DBOS Cloud and Conductor are a possible later follow-on; nothing here depends on them.
|
|
188
|
+
|
|
189
|
+
### Do I need anything besides Postgres?
|
|
190
|
+
|
|
191
|
+
No. By default DBOS keeps its system tables in Long Tail's database, in the `dbos` schema, so one Postgres database serves both. Separate databases work too: set `DBOS_SYSTEM_DATABASE_URL`. The Long Tail container needs no other service. If you already run NATS for Long Tail events you can keep it, but it is optional.
|
|
192
|
+
|
|
193
|
+
### What is the license?
|
|
194
|
+
|
|
195
|
+
Source available: Apache 2.0 with the Commons Clause.
|
|
196
|
+
- You can use it, change it and run it in production for your own work.
|
|
197
|
+
- You can build and sell your own products on top of it.
|
|
198
|
+
- You cannot sell this package itself, or a hosted service built mainly from it. Only HotMeshIO does that.
|
|
199
|
+
|
|
200
|
+
See [LICENSE](LICENSE); its summary is a convenience and the license text governs.
|
|
201
|
+
|
|
202
|
+
### What is out of scope today?
|
|
203
|
+
|
|
204
|
+
- Batch and accumulate escalations, where one row collects many items.
|
|
205
|
+
- Starting DBOS workflows from Long Tail.
|
|
206
|
+
- DBOS schedules.
|
|
207
|
+
- Running HotMesh and DBOS workflows in one deployment.
|
|
208
|
+
- DBOS suspend mode.
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npm install
|
|
214
|
+
npm test # unit tests, no services needed
|
|
215
|
+
npm run test:integration # Long Tail and DBOS in process against Postgres
|
|
216
|
+
npm run test:e2e # the running stack over HTTP (docker compose up -d --build)
|
|
217
|
+
npm run build
|
|
218
|
+
npm run probe # re-records tests/fixtures/dbos-run.json from a real DBOS run
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- The integration tests use this repo's Postgres (`docker compose up -d postgres`, port 5425; set `PG_PORT` to point elsewhere). They create and drop only their own database, `lt_dbos_e2e`.
|
|
222
|
+
- The end-to-end tests drive the compose stack the way a deployment runs: they start reviews through the app on port 4000 and work them through Long Tail on port 3400 as people and bots would. `LT_URL`, `APP_URL`, `LT_ADMIN_USER` and `LT_ADMIN_PASSWORD` point them elsewhere. Each run uses unique ids, so it can run repeatedly against one stack.
|
|
223
|
+
- The image installs `@hotmeshio/long-tail` from npm. To try an unreleased build, run `./scripts/pack-long-tail.sh ../long-tail`; the image then installs `vendor/*.tgz` instead.
|
|
224
|
+
|
|
225
|
+
### Releasing
|
|
226
|
+
|
|
227
|
+
CI runs on every pull request and every push to `main`. To release, bump `version` in `package.json` and merge to `main`. The Release workflow runs CI again, publishes the version to npm with provenance, tags `v<version>` and creates a GitHub release. A version npm already has is skipped.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LTConditionalConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Wait for a person (or machine) through Long Tail.
|
|
4
|
+
*
|
|
5
|
+
* Opens an escalation in `config.role`, parks the workflow in DBOS.recv, and
|
|
6
|
+
* returns the resolver payload. Returns null when the escalation expires or
|
|
7
|
+
* is cancelled. `key` is unique per wait within one workflow.
|
|
8
|
+
*/
|
|
9
|
+
export declare function conditional<T = Record<string, unknown>>(key: string, config: LTConditionalConfig): Promise<T | null>;
|
|
10
|
+
/** recv deadline: the SLA plus a grace period, or the max wait when there is no SLA. */
|
|
11
|
+
export declare function waitSeconds(config: LTConditionalConfig): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.conditional = conditional;
|
|
4
|
+
exports.waitSeconds = waitSeconds;
|
|
5
|
+
const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const duration_1 = require("./duration");
|
|
8
|
+
const steps_1 = require("./steps");
|
|
9
|
+
/**
|
|
10
|
+
* Wait for a person (or machine) through Long Tail.
|
|
11
|
+
*
|
|
12
|
+
* Opens an escalation in `config.role`, parks the workflow in DBOS.recv, and
|
|
13
|
+
* returns the resolver payload. Returns null when the escalation expires or
|
|
14
|
+
* is cancelled. `key` is unique per wait within one workflow.
|
|
15
|
+
*/
|
|
16
|
+
async function conditional(key, config) {
|
|
17
|
+
if (!key)
|
|
18
|
+
throw new Error('conditional(key): key is required');
|
|
19
|
+
if (!config?.role || !config.type)
|
|
20
|
+
throw new Error('conditional(key, config): role and type are required');
|
|
21
|
+
if (!dbos_sdk_1.DBOS.isInWorkflow())
|
|
22
|
+
throw new Error('conditional() must run in a DBOS workflow body, not inside a step');
|
|
23
|
+
// 1. Open the wait (idempotent on workflowId + key)
|
|
24
|
+
const opened = await (0, steps_1.ltOpen)(key, config);
|
|
25
|
+
// 2. Park until Long Tail delivers; DBOS defaults recv to 60 s, so the deadline is explicit
|
|
26
|
+
const outcome = await dbos_sdk_1.DBOS.recv(key, { timeoutSeconds: waitSeconds(config) });
|
|
27
|
+
// 3. No outcome means the backstop passed first: close the row
|
|
28
|
+
if (!outcome) {
|
|
29
|
+
dbos_sdk_1.DBOS.logger.warn(`long-tail-dbos: ${key} reached its recv deadline (${constants_1.CLOSE_REASON_DEADLINE}); cancelling ${opened.escalationId}`);
|
|
30
|
+
await (0, steps_1.ltClose)(opened.escalationId);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return outcome.status === 'resolved' ? (outcome.data ?? {}) : null;
|
|
34
|
+
}
|
|
35
|
+
/** recv deadline: the SLA plus a grace period, or the max wait when there is no SLA. */
|
|
36
|
+
function waitSeconds(config) {
|
|
37
|
+
if (config.timeout !== undefined) {
|
|
38
|
+
return Math.ceil((0, duration_1.toMs)(config.timeout) / 1000) + constants_1.WAIT_DEFAULTS.GRACE_SECONDS;
|
|
39
|
+
}
|
|
40
|
+
return config.maxWaitSeconds ?? constants_1.WAIT_DEFAULTS.MAX_WAIT_SECONDS;
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LongTailConnection } from './types';
|
|
2
|
+
/** Point the shim at Long Tail. Call once at startup, before DBOS.launch(). */
|
|
3
|
+
export declare function configureLongTail(config: LongTailConnection): void;
|
|
4
|
+
export declare function getLongTailConnection(): LongTailConnection;
|
|
5
|
+
/** For tests. */
|
|
6
|
+
export declare function resetLongTailConnection(): void;
|
package/build/config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureLongTail = configureLongTail;
|
|
4
|
+
exports.getLongTailConnection = getLongTailConnection;
|
|
5
|
+
exports.resetLongTailConnection = resetLongTailConnection;
|
|
6
|
+
let connection = null;
|
|
7
|
+
/** Point the shim at Long Tail. Call once at startup, before DBOS.launch(). */
|
|
8
|
+
function configureLongTail(config) {
|
|
9
|
+
if (!config?.url || typeof config.url !== 'string')
|
|
10
|
+
throw new Error('configureLongTail: url is required');
|
|
11
|
+
if (!config.apiKey || typeof config.apiKey !== 'string')
|
|
12
|
+
throw new Error('configureLongTail: apiKey is required');
|
|
13
|
+
try {
|
|
14
|
+
new URL(config.url);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`configureLongTail: url "${config.url}" is not a valid URL`);
|
|
18
|
+
}
|
|
19
|
+
connection = { ...config, url: config.url.replace(/\/+$/, '') };
|
|
20
|
+
}
|
|
21
|
+
function getLongTailConnection() {
|
|
22
|
+
if (!connection)
|
|
23
|
+
throw new Error('Long Tail is not configured: call configureLongTail({ url, apiKey }) before conditional()');
|
|
24
|
+
return connection;
|
|
25
|
+
}
|
|
26
|
+
/** For tests. */
|
|
27
|
+
function resetLongTailConnection() {
|
|
28
|
+
connection = null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Step names the shim registers; they appear in the DBOS step list. */
|
|
2
|
+
export declare const STEP: {
|
|
3
|
+
readonly OPEN: "lt.open";
|
|
4
|
+
readonly CLOSE: "lt.close";
|
|
5
|
+
};
|
|
6
|
+
/** Reserved DBOS operation names (they appear in listWorkflowSteps). */
|
|
7
|
+
export declare const DBOS_OP: {
|
|
8
|
+
readonly RECV: "DBOS.recv";
|
|
9
|
+
readonly SLEEP: "DBOS.sleep";
|
|
10
|
+
readonly SEND: "DBOS.send";
|
|
11
|
+
readonly GET_RESULT: "DBOS.getResult";
|
|
12
|
+
readonly GET_STATUS: "getStatus";
|
|
13
|
+
};
|
|
14
|
+
/** DBOS operations that are bookkeeping, not work. */
|
|
15
|
+
export declare const DBOS_SYSTEM_STEPS: readonly string[];
|
|
16
|
+
export declare const PROVIDER_NAME = "dbos";
|
|
17
|
+
export declare const WAIT_DEFAULTS: {
|
|
18
|
+
/** Added to the SLA so Long Tail's expiry reaches the workflow before the recv deadline. */
|
|
19
|
+
readonly GRACE_SECONDS: number;
|
|
20
|
+
/** recv deadline when the wait has no SLA. */
|
|
21
|
+
readonly MAX_WAIT_SECONDS: number;
|
|
22
|
+
readonly OPEN_ATTEMPTS: 10;
|
|
23
|
+
readonly REQUEST_TIMEOUT_MS: 15000;
|
|
24
|
+
};
|
|
25
|
+
export declare const LT_PATHS: {
|
|
26
|
+
readonly OPEN_WAIT: "/api/escalations/waits";
|
|
27
|
+
readonly CANCEL: (escalationId: string) => string;
|
|
28
|
+
};
|
|
29
|
+
export declare const STEP_PAGE_SIZE = 500;
|
|
30
|
+
/** Connections the provider keeps for counting workflows in the executions list. */
|
|
31
|
+
export declare const COUNT_POOL_SIZE = 2;
|
|
32
|
+
/** Reason sent when the recv deadline passes before Long Tail delivered an outcome. */
|
|
33
|
+
export declare const CLOSE_REASON_DEADLINE = "wait-deadline";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLOSE_REASON_DEADLINE = exports.COUNT_POOL_SIZE = exports.STEP_PAGE_SIZE = exports.LT_PATHS = exports.WAIT_DEFAULTS = exports.PROVIDER_NAME = exports.DBOS_SYSTEM_STEPS = exports.DBOS_OP = exports.STEP = void 0;
|
|
4
|
+
/** Step names the shim registers; they appear in the DBOS step list. */
|
|
5
|
+
exports.STEP = {
|
|
6
|
+
OPEN: 'lt.open',
|
|
7
|
+
CLOSE: 'lt.close',
|
|
8
|
+
};
|
|
9
|
+
/** Reserved DBOS operation names (they appear in listWorkflowSteps). */
|
|
10
|
+
exports.DBOS_OP = {
|
|
11
|
+
RECV: 'DBOS.recv',
|
|
12
|
+
SLEEP: 'DBOS.sleep',
|
|
13
|
+
SEND: 'DBOS.send',
|
|
14
|
+
GET_RESULT: 'DBOS.getResult',
|
|
15
|
+
GET_STATUS: 'getStatus',
|
|
16
|
+
};
|
|
17
|
+
/** DBOS operations that are bookkeeping, not work. */
|
|
18
|
+
exports.DBOS_SYSTEM_STEPS = [
|
|
19
|
+
'DBOS.now',
|
|
20
|
+
'DBOS.randomUUID',
|
|
21
|
+
'DBOS.setEvent',
|
|
22
|
+
'DBOS.getEvent',
|
|
23
|
+
'DBOS.writeStream',
|
|
24
|
+
'DBOS.closeStream',
|
|
25
|
+
'DBOS.readStream',
|
|
26
|
+
'DBOS.send',
|
|
27
|
+
'getStatus',
|
|
28
|
+
];
|
|
29
|
+
exports.PROVIDER_NAME = 'dbos';
|
|
30
|
+
exports.WAIT_DEFAULTS = {
|
|
31
|
+
/** Added to the SLA so Long Tail's expiry reaches the workflow before the recv deadline. */
|
|
32
|
+
GRACE_SECONDS: 10 * 60,
|
|
33
|
+
/** recv deadline when the wait has no SLA. */
|
|
34
|
+
MAX_WAIT_SECONDS: 365 * 24 * 60 * 60,
|
|
35
|
+
OPEN_ATTEMPTS: 10,
|
|
36
|
+
REQUEST_TIMEOUT_MS: 15_000,
|
|
37
|
+
};
|
|
38
|
+
exports.LT_PATHS = {
|
|
39
|
+
OPEN_WAIT: '/api/escalations/waits',
|
|
40
|
+
CANCEL: (escalationId) => `/api/escalations/${encodeURIComponent(escalationId)}/cancel`,
|
|
41
|
+
};
|
|
42
|
+
exports.STEP_PAGE_SIZE = 500;
|
|
43
|
+
/** Connections the provider keeps for counting workflows in the executions list. */
|
|
44
|
+
exports.COUNT_POOL_SIZE = 2;
|
|
45
|
+
/** Reason sent when the recv deadline passes before Long Tail delivered an outcome. */
|
|
46
|
+
exports.CLOSE_REASON_DEADLINE = 'wait-deadline';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toMs = toMs;
|
|
4
|
+
const UNIT_MS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
5
|
+
const DURATION = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
6
|
+
/** Milliseconds from '48h', '30m', '10s', '2d', '500ms' or a number of milliseconds. */
|
|
7
|
+
function toMs(value) {
|
|
8
|
+
if (typeof value === 'number') {
|
|
9
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
10
|
+
throw new Error(`duration must be a positive number of milliseconds, got ${value}`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
const match = DURATION.exec(value.trim());
|
|
14
|
+
if (!match)
|
|
15
|
+
throw new Error(`duration "${value}" must look like 48h, 30m, 10s, 2d or 500ms`);
|
|
16
|
+
const ms = Number(match[1]) * UNIT_MS[match[2]];
|
|
17
|
+
if (ms <= 0)
|
|
18
|
+
throw new Error(`duration "${value}" must be positive`);
|
|
19
|
+
return ms;
|
|
20
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { conditional, waitSeconds } from './conditional';
|
|
2
|
+
export { configureLongTail } from './config';
|
|
3
|
+
export { LongTailRequestError } from './lt-client';
|
|
4
|
+
export { toMs } from './duration';
|
|
5
|
+
export { STEP, WAIT_DEFAULTS } from './constants';
|
|
6
|
+
export type { LTConditionalConfig, LTWaitOutcome, LongTailConnection, OpenedWait } from './types';
|