rethread 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +43 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# rethread
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/rethread)
|
|
4
|
+
[](https://www.npmjs.com/package/rethread)
|
|
5
|
+
[](https://www.npmjs.com/package/rethread)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
**One function for multi-step workflows over stateless transports.** An
|
|
9
|
+
onboarding wizard, a checkout funnel, an approval flow, an SMS or chat bot or any
|
|
10
|
+
process that advances one request → reply at a time.
|
|
11
|
+
|
|
12
|
+
## Getting started
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { rethread } from 'rethread';
|
|
16
|
+
|
|
17
|
+
const signup = rethread(function* (suspend) {
|
|
18
|
+
const email = yield* suspend(() => "Welcome! What's your email?");
|
|
19
|
+
const plan = yield* suspend(() => 'Pick a plan: (1) Free (2) Pro');
|
|
20
|
+
|
|
21
|
+
if (plan === '2') {
|
|
22
|
+
const card = yield* suspend(() => "Great - what's your card number?");
|
|
23
|
+
return `You're on Pro, ${email} - charged to ••••${card.slice(-4)}.`;
|
|
24
|
+
}
|
|
25
|
+
return `You're on Free, ${email}. Welcome aboard!`;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// The caller - usually an http handler, slack callback, etc.
|
|
29
|
+
handleNewSms(async (msg, respond) => {
|
|
30
|
+
const state = await db.getState(msg.sessionId);
|
|
31
|
+
const events = [...state, msg.text];
|
|
32
|
+
const { value, done, events: newEvents } = signup(events);
|
|
33
|
+
await db.saveState(msg.sessionId, newEvents);
|
|
34
|
+
respond(value);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The workflow is a generator. `yield* suspend(() => whatToSend)` halts the workflow and
|
|
39
|
+
returns `whatToSend` to the caller. The next time the workflow runs with the updated
|
|
40
|
+
log, it picks up right after that `yield*` with the reply. All events are strings
|
|
41
|
+
since they need to be persisted and replayed.
|
|
42
|
+
|
|
43
|
+
See `test/` for more advanced patterns (back, time-travel, recording nondeterminism, async).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumable flows via stateless generator replay. The only state is `events` (the
|
|
3
|
+
* replies so far); each call replays the body over it, fast-forwarding answered
|
|
4
|
+
* `suspend`s and stopping at the first un-answered one.
|
|
5
|
+
*
|
|
6
|
+
* const turn = rethread(function* (suspend) {
|
|
7
|
+
* const name = yield* suspend(() => "Enter your name?");
|
|
8
|
+
* return `Hi ${name}`;
|
|
9
|
+
* });
|
|
10
|
+
* const { value, done, events } = turn(events);
|
|
11
|
+
*/
|
|
12
|
+
declare function suspend<O>(render: () => O): Generator<() => O, string, string>;
|
|
13
|
+
type GetEvents = () => string[];
|
|
14
|
+
type Turn<Y, R> = {
|
|
15
|
+
value: Y;
|
|
16
|
+
done: false;
|
|
17
|
+
events: string[];
|
|
18
|
+
} | {
|
|
19
|
+
value: R;
|
|
20
|
+
done: true;
|
|
21
|
+
events: string[];
|
|
22
|
+
};
|
|
23
|
+
type Unwrap<T> = T extends () => infer O ? O : T;
|
|
24
|
+
type HasAsync<T> = Extract<T, Promise<unknown>> extends never ? false : true;
|
|
25
|
+
type SyncResult<Y, R> = HasAsync<Unwrap<Y>> extends true ? Promise<Turn<Awaited<Unwrap<Y>>, R>> : Turn<Unwrap<Y>, R>;
|
|
26
|
+
/**
|
|
27
|
+
* Sync body → `Turn`; async body → `Promise<Turn>`. If any render is async (even in a
|
|
28
|
+
* sync body), the return promotes to `Promise<Turn>` so the caller must `await`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function rethread<Y, R>(body: (s: typeof suspend, getEvents: GetEvents) => Generator<Y, R, string>): (events?: string[]) => SyncResult<Y, R>;
|
|
31
|
+
export declare function rethread<Y, R>(body: (s: typeof suspend, getEvents: GetEvents) => AsyncGenerator<Y, R, string>): (events?: string[]) => Promise<Turn<Awaited<Unwrap<Y>>, Awaited<R>>>;
|
|
32
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rethread = rethread;
|
|
4
|
+
/**
|
|
5
|
+
* Resumable flows via stateless generator replay. The only state is `events` (the
|
|
6
|
+
* replies so far); each call replays the body over it, fast-forwarding answered
|
|
7
|
+
* `suspend`s and stopping at the first un-answered one.
|
|
8
|
+
*
|
|
9
|
+
* const turn = rethread(function* (suspend) {
|
|
10
|
+
* const name = yield* suspend(() => "Enter your name?");
|
|
11
|
+
* return `Hi ${name}`;
|
|
12
|
+
* });
|
|
13
|
+
* const { value, done, events } = turn(events);
|
|
14
|
+
*/
|
|
15
|
+
function* suspend(render) {
|
|
16
|
+
return yield render;
|
|
17
|
+
}
|
|
18
|
+
const isThenable = (x) => x != null && typeof x.then === 'function';
|
|
19
|
+
function rethread(body) {
|
|
20
|
+
return (events = []) => {
|
|
21
|
+
let i = 0;
|
|
22
|
+
const gen = body(suspend, () => events.slice(0, i));
|
|
23
|
+
const finish = (value, done) => isThenable(value)
|
|
24
|
+
? Promise.resolve(value).then((v) => ({ value: v, done, events }))
|
|
25
|
+
: { value, done, events };
|
|
26
|
+
const pump = (res) => {
|
|
27
|
+
while (!isThenable(res)) {
|
|
28
|
+
if (res.done)
|
|
29
|
+
return finish(res.value, true);
|
|
30
|
+
if (i < events.length) {
|
|
31
|
+
res = gen.next(events[i++]);
|
|
32
|
+
continue;
|
|
33
|
+
} // fast-forward
|
|
34
|
+
const cb = res.value;
|
|
35
|
+
if (typeof cb === 'function')
|
|
36
|
+
return finish(cb(), false);
|
|
37
|
+
return finish(cb, false);
|
|
38
|
+
}
|
|
39
|
+
return res.then(pump);
|
|
40
|
+
};
|
|
41
|
+
return pump(gen.next());
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rethread",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "One function for multi-step workflows over stateless transports",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "node --disable-warning=ExperimentalWarning --experimental-transform-types --test \"test/**/*.test.ts\"",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"author": "Moshe Kolodny",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^25.9.1",
|
|
18
|
+
"typescript": "^6.0.3"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/kolodny/rethread.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/kolodny/rethread/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/kolodny/rethread#readme"
|
|
28
|
+
}
|