@reboot-dev/reboot 0.43.0 → 0.45.1
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 +160 -0
- package/index.d.ts +25 -4
- package/index.js +72 -7
- package/package.json +3 -4
- package/reboot_native.cc +239 -44
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/zod-to-proto.js +136 -5
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://docs.reboot.dev/img/reboot-logo-green.svg"
|
|
4
|
+
alt="Reboot" width="200" />
|
|
5
|
+
|
|
6
|
+
# Reboot
|
|
7
|
+
|
|
8
|
+
**Build AI Chat Apps — and full-stack web apps — with reactive, durable backends.**
|
|
9
|
+
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://pypi.org/project/reboot/)
|
|
12
|
+
[](https://www.npmjs.com/package/@reboot-dev/reboot)
|
|
13
|
+
[](https://discord.gg/cRbdcS94Nr)
|
|
14
|
+
[](https://docs.reboot.dev/)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Reboot is a framework for building **reactive, stateful, multiplayer AI chat
|
|
21
|
+
apps** — visual apps that run inside ChatGPT, Claude, VS Code, Goose, and more.
|
|
22
|
+
It also builds full-stack web apps with reactive backends and React frontends.
|
|
23
|
+
|
|
24
|
+
With Reboot, you just write business logic — no wiring up databases, caches,
|
|
25
|
+
queues, or retry loops. State survives failures by default. ACID transactions
|
|
26
|
+
span multiple states. The React frontend stays in sync in real time. And your
|
|
27
|
+
backend is automatically an MCP server.
|
|
28
|
+
|
|
29
|
+
## AI Chat Apps
|
|
30
|
+
|
|
31
|
+
Build visual, interactive apps that run inside AI chat interfaces. Define a
|
|
32
|
+
`Session` type as an entry point and your methods automatically become tools
|
|
33
|
+
the AI can call:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from reboot.api import (
|
|
37
|
+
API, Field, Methods, Model, Reader, Tool,
|
|
38
|
+
Transaction, Type, UI, Writer,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CreateCounterResponse(Model):
|
|
43
|
+
counter_id: str = Field(tag=1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SessionState(Model):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CounterState(Model):
|
|
51
|
+
value: int = Field(tag=1, default=0)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GetResponse(Model):
|
|
55
|
+
value: int = Field(tag=1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class IncrementRequest(Model):
|
|
59
|
+
"""Request with an amount parameter."""
|
|
60
|
+
amount: int | None = Field(tag=1, default=None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
api = API(
|
|
64
|
+
Session=Type(
|
|
65
|
+
state=SessionState,
|
|
66
|
+
methods=Methods(
|
|
67
|
+
create_counter=Transaction(
|
|
68
|
+
request=None,
|
|
69
|
+
response=CreateCounterResponse,
|
|
70
|
+
description="Create a new Counter.",
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
Counter=Type(
|
|
75
|
+
state=CounterState,
|
|
76
|
+
methods=Methods(
|
|
77
|
+
show_clicker=UI(
|
|
78
|
+
request=None,
|
|
79
|
+
path="web/ui/clicker",
|
|
80
|
+
title="Counter Clicker",
|
|
81
|
+
description="Interactive clicker UI.",
|
|
82
|
+
),
|
|
83
|
+
create=Writer(
|
|
84
|
+
request=None,
|
|
85
|
+
response=None,
|
|
86
|
+
factory=True,
|
|
87
|
+
),
|
|
88
|
+
get=Reader(
|
|
89
|
+
request=None,
|
|
90
|
+
response=GetResponse,
|
|
91
|
+
description="Get the current counter "
|
|
92
|
+
"value.",
|
|
93
|
+
mcp=Tool(),
|
|
94
|
+
),
|
|
95
|
+
increment=Writer(
|
|
96
|
+
request=IncrementRequest,
|
|
97
|
+
response=None,
|
|
98
|
+
description="Increment the counter.",
|
|
99
|
+
mcp=Tool(),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Dive in!
|
|
107
|
+
|
|
108
|
+
- [What is an AI Chat App?](https://docs.reboot.dev/ai_chat_apps/what_is)
|
|
109
|
+
- [Get Started (Python)](https://docs.reboot.dev/ai_chat_apps/get_started)
|
|
110
|
+
- [AI Chat App Examples](https://docs.reboot.dev/ai_chat_apps/examples)
|
|
111
|
+
|
|
112
|
+
## Full-stack apps
|
|
113
|
+
|
|
114
|
+
Build reactive backends with React frontends — great as a full-page extension
|
|
115
|
+
of your AI chat app, or as a standalone web app.
|
|
116
|
+
|
|
117
|
+
- [TypeScript Quickstart](https://docs.reboot.dev/full_stack_apps/typescript)
|
|
118
|
+
- [Python Quickstart](https://docs.reboot.dev/full_stack_apps/python)
|
|
119
|
+
- [Full-stack Examples](https://docs.reboot.dev/full_stack_apps/examples)
|
|
120
|
+
|
|
121
|
+
## Key features
|
|
122
|
+
|
|
123
|
+
**Automatic MCP server.** `Session` methods are automatically exposed as
|
|
124
|
+
MCP tools. Other types can opt in with `mcp=Tool()`. `UI` methods open
|
|
125
|
+
React apps in the AI's chat. No glue code.
|
|
126
|
+
|
|
127
|
+
**Durable state by default.** States survive process crashes, deployments, and
|
|
128
|
+
chaos. No external database required.
|
|
129
|
+
|
|
130
|
+
**ACID transactions across states.** `transaction` methods compose atomically
|
|
131
|
+
across many state instances running on different machines.
|
|
132
|
+
|
|
133
|
+
**Reactive React frontend.** Generated hooks keep your UI in sync
|
|
134
|
+
without manual management of WebSockets, caches, or polling.
|
|
135
|
+
|
|
136
|
+
**Method system.** Code is safer to write (and _read_) with a clear API
|
|
137
|
+
and methods with enforced constraints: `reader` (concurrent, read-only),
|
|
138
|
+
`writer` (serialized, mutating), `transaction` (ACID, cross-state),
|
|
139
|
+
`workflow` (long-running, durable, cancellable), `ui` (React app in AI
|
|
140
|
+
chat). The runtime enforces these guarantees.
|
|
141
|
+
|
|
142
|
+
**API-first, code-generated.** Define APIs using Pydantic (Python) or
|
|
143
|
+
Zod (TypeScript). Reboot generates type-safe client, server, and React
|
|
144
|
+
stubs.
|
|
145
|
+
|
|
146
|
+
## Documentation
|
|
147
|
+
|
|
148
|
+
Full documentation at [docs.reboot.dev](https://docs.reboot.dev/).
|
|
149
|
+
|
|
150
|
+
## Community
|
|
151
|
+
|
|
152
|
+
- **Discord**: [discord.gg/cRbdcS94Nr](https://discord.gg/cRbdcS94Nr) — fastest way to get help or share what you're building
|
|
153
|
+
- **Issues**: [github.com/reboot-dev/reboot/issues](https://github.com/reboot-dev/reboot/issues)
|
|
154
|
+
|
|
155
|
+
Contributions are welcome. Open an issue to discuss substantial changes before
|
|
156
|
+
sending a pull request.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[Apache 2.0](LICENSE)
|
package/index.d.ts
CHANGED
|
@@ -60,7 +60,8 @@ export declare class Context {
|
|
|
60
60
|
readonly cookie: string | null;
|
|
61
61
|
readonly appInternal: boolean;
|
|
62
62
|
readonly auth: Auth | null;
|
|
63
|
-
|
|
63
|
+
readonly workflowId: string | null;
|
|
64
|
+
constructor({ external, stateId, method, stateTypeName, callerBearerToken, cookie, appInternal, auth, workflowId, cancelled, }: {
|
|
64
65
|
external: any;
|
|
65
66
|
stateId: string;
|
|
66
67
|
method: string;
|
|
@@ -69,6 +70,7 @@ export declare class Context {
|
|
|
69
70
|
cookie: string | null;
|
|
70
71
|
appInternal: boolean;
|
|
71
72
|
auth: Auth | null;
|
|
73
|
+
workflowId: string | null;
|
|
72
74
|
cancelled: Promise<void>;
|
|
73
75
|
});
|
|
74
76
|
static fromNativeExternal({ kind, ...options }: {
|
|
@@ -101,6 +103,10 @@ export type Interval = {
|
|
|
101
103
|
export declare class WorkflowContext extends Context {
|
|
102
104
|
#private;
|
|
103
105
|
constructor(options: any);
|
|
106
|
+
makeIdempotencyKey({ alias, perWorkflow, }: {
|
|
107
|
+
alias: string;
|
|
108
|
+
perWorkflow?: boolean;
|
|
109
|
+
}): string;
|
|
104
110
|
loop(alias: string, { interval }?: {
|
|
105
111
|
interval?: Interval;
|
|
106
112
|
}): AsyncGenerator<number, void, unknown>;
|
|
@@ -178,7 +184,7 @@ export declare abstract class Authorizer<StateType, RequestTypes> {
|
|
|
178
184
|
* `errors_pb.PermissionDenied()` otherwise.
|
|
179
185
|
*/
|
|
180
186
|
abstract authorize(methodName: string, context: ReaderContext, state?: StateType, request?: RequestTypes): Promise<AuthorizerDecision>;
|
|
181
|
-
_authorize
|
|
187
|
+
abstract _authorize(external: any, cancelled: Promise<void>, bytesCall: Uint8Array): Promise<Uint8Array>;
|
|
182
188
|
}
|
|
183
189
|
export type AuthorizerCallable<StateType, RequestType> = (args: {
|
|
184
190
|
context: ReaderContext;
|
|
@@ -212,15 +218,30 @@ export declare function isAppInternal({ context, }: {
|
|
|
212
218
|
state?: any;
|
|
213
219
|
request?: any;
|
|
214
220
|
}): errors_pb.PermissionDenied | errors_pb.Ok;
|
|
221
|
+
export type NativeLibrary = {
|
|
222
|
+
nativeLibraryModule: string;
|
|
223
|
+
nativeLibraryFunction: string;
|
|
224
|
+
authorizer?: Authorizer<unknown, unknown>;
|
|
225
|
+
};
|
|
226
|
+
export type TypeScriptLibrary = {
|
|
227
|
+
name: string;
|
|
228
|
+
servicers: () => ServicerFactory[];
|
|
229
|
+
requirements?: () => string[];
|
|
230
|
+
preRun?: (application: Application) => Promise<void>;
|
|
231
|
+
initialize?: (context: InitializeContext) => Promise<void>;
|
|
232
|
+
};
|
|
233
|
+
export type Library = TypeScriptLibrary | NativeLibrary;
|
|
215
234
|
export declare class Application {
|
|
216
235
|
#private;
|
|
217
|
-
constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }: {
|
|
218
|
-
servicers
|
|
236
|
+
constructor({ servicers, libraries, initialize, initializeBearerToken, tokenVerifier, }: {
|
|
237
|
+
servicers?: ServicerFactory[];
|
|
238
|
+
libraries?: Library[];
|
|
219
239
|
initialize?: (context: InitializeContext) => Promise<void>;
|
|
220
240
|
initializeBearerToken?: string;
|
|
221
241
|
tokenVerifier?: TokenVerifier;
|
|
222
242
|
});
|
|
223
243
|
get servicers(): ServicerFactory[];
|
|
244
|
+
get libraries(): Library[];
|
|
224
245
|
get tokenVerifier(): TokenVerifier;
|
|
225
246
|
get http(): Application.Http;
|
|
226
247
|
run(): Promise<any>;
|
package/index.js
CHANGED
|
@@ -9,12 +9,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
|
-
var _Reboot_external, _ExternalContext_external, _a, _Context_external, _Context_isInternalConstructing, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_servicers, _Application_tokenVerifier, _Application_express, _Application_http, _Application_servers, _Application_createExternalContext, _Application_external;
|
|
12
|
+
var _Reboot_external, _ExternalContext_external, _a, _Context_external, _Context_isInternalConstructing, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_servicers, _Application_libraries, _Application_tokenVerifier, _Application_express, _Application_http, _Application_servers, _Application_createExternalContext, _Application_external;
|
|
13
13
|
import { assert, auth_pb, check_bufbuild_protobuf_library, errors_pb, nodejs_pb, parse, protobuf_es, stringify, tasks_pb, toCamelCase, } from "@reboot-dev/reboot-api";
|
|
14
14
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
15
|
import { fork } from "node:child_process";
|
|
16
16
|
import { createRequire } from "node:module";
|
|
17
17
|
import toobusy from "toobusy-js";
|
|
18
|
+
import { v5 as uuidv5 } from "uuid";
|
|
18
19
|
import { z } from "zod/v4";
|
|
19
20
|
import * as reboot_native from "./reboot_native.cjs";
|
|
20
21
|
import { ensureError } from "./utils/errors.js";
|
|
@@ -169,7 +170,7 @@ export async function runWithContext(context, callback) {
|
|
|
169
170
|
}, callback);
|
|
170
171
|
}
|
|
171
172
|
export class Context {
|
|
172
|
-
constructor({ external, stateId, method, stateTypeName, callerBearerToken, cookie, appInternal, auth, cancelled, }) {
|
|
173
|
+
constructor({ external, stateId, method, stateTypeName, callerBearerToken, cookie, appInternal, auth, workflowId, cancelled, }) {
|
|
173
174
|
_Context_external.set(this, void 0);
|
|
174
175
|
if (!__classPrivateFieldGet(_a, _a, "f", _Context_isInternalConstructing)) {
|
|
175
176
|
throw new TypeError("Context is not publicly constructable");
|
|
@@ -185,6 +186,7 @@ export class Context {
|
|
|
185
186
|
this.cookie = cookie;
|
|
186
187
|
this.appInternal = appInternal;
|
|
187
188
|
this.auth = auth;
|
|
189
|
+
this.workflowId = workflowId;
|
|
188
190
|
this.cancelled = cancelled;
|
|
189
191
|
}
|
|
190
192
|
static fromNativeExternal({ kind, ...options }) {
|
|
@@ -263,6 +265,17 @@ export class WorkflowContext extends Context {
|
|
|
263
265
|
_WorkflowContext_kind.set(this, "workflow");
|
|
264
266
|
}
|
|
265
267
|
// TODO: implement workflow specific properties/methods.
|
|
268
|
+
makeIdempotencyKey({ alias, perWorkflow, }) {
|
|
269
|
+
assert(this.workflowId !== null);
|
|
270
|
+
if (!perWorkflow) {
|
|
271
|
+
const store = contextStorage.getStore();
|
|
272
|
+
assert(store !== undefined);
|
|
273
|
+
if (store.withinLoop) {
|
|
274
|
+
alias += ` (iteration #${store.loopIteration})`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return uuidv5(alias, this.workflowId);
|
|
278
|
+
}
|
|
266
279
|
async *loop(alias, { interval } = {}) {
|
|
267
280
|
const iterate = await reboot_native.WorkflowContext_loop(this.__external, alias);
|
|
268
281
|
const ms = (interval &&
|
|
@@ -384,6 +397,7 @@ export class TokenVerifier {
|
|
|
384
397
|
cookie: call.context.cookie,
|
|
385
398
|
appInternal: call.context.appInternal,
|
|
386
399
|
auth: null,
|
|
400
|
+
workflowId: call.context.workflowId !== undefined ? call.context.workflowId : null,
|
|
387
401
|
cancelled,
|
|
388
402
|
});
|
|
389
403
|
const auth = await this.verifyToken(context, call.token);
|
|
@@ -506,16 +520,52 @@ export function isAppInternal({ context, }) {
|
|
|
506
520
|
}
|
|
507
521
|
return new errors_pb.PermissionDenied();
|
|
508
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Prepares libraries for conversion. Wraps `initialize` method for better
|
|
525
|
+
* error handling.
|
|
526
|
+
*
|
|
527
|
+
* @param library Library to be prepared
|
|
528
|
+
* @returns
|
|
529
|
+
*/
|
|
530
|
+
function prepareLibrary(library) {
|
|
531
|
+
// Skip Python libraries and libraries without an initialize.
|
|
532
|
+
if (library.nativeLibraryModule !== undefined ||
|
|
533
|
+
library.initialize === undefined) {
|
|
534
|
+
return library;
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
...library,
|
|
538
|
+
initialize: async (context) => {
|
|
539
|
+
try {
|
|
540
|
+
await library.initialize(context);
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
// Ensure we have an `Error` and then `console.error()` it
|
|
544
|
+
// so that developers see a stack trace of what is going
|
|
545
|
+
// on.
|
|
546
|
+
const error = ensureError(e);
|
|
547
|
+
// Write an empty message which includes a newline to make
|
|
548
|
+
// it easier to identify the stack trace.
|
|
549
|
+
console.error("");
|
|
550
|
+
console.error(error);
|
|
551
|
+
console.error("");
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
509
557
|
export class Application {
|
|
510
|
-
constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }) {
|
|
558
|
+
constructor({ servicers, libraries, initialize, initializeBearerToken, tokenVerifier, }) {
|
|
511
559
|
_Application_servicers.set(this, void 0);
|
|
560
|
+
_Application_libraries.set(this, void 0);
|
|
512
561
|
_Application_tokenVerifier.set(this, void 0);
|
|
513
562
|
_Application_express.set(this, void 0);
|
|
514
563
|
_Application_http.set(this, void 0);
|
|
515
564
|
_Application_servers.set(this, void 0);
|
|
516
565
|
_Application_createExternalContext.set(this, void 0);
|
|
517
566
|
_Application_external.set(this, void 0);
|
|
518
|
-
__classPrivateFieldSet(this, _Application_servicers, servicers, "f");
|
|
567
|
+
__classPrivateFieldSet(this, _Application_servicers, servicers || [], "f");
|
|
568
|
+
__classPrivateFieldSet(this, _Application_libraries, libraries || [], "f");
|
|
519
569
|
__classPrivateFieldSet(this, _Application_tokenVerifier, tokenVerifier, "f");
|
|
520
570
|
__classPrivateFieldSet(this, _Application_express, express(), "f");
|
|
521
571
|
// We assume that our users will want these middleware.
|
|
@@ -533,7 +583,7 @@ export class Application {
|
|
|
533
583
|
}
|
|
534
584
|
assert(kind === "initialize");
|
|
535
585
|
return InitializeContext.fromNativeExternal(external);
|
|
536
|
-
},
|
|
586
|
+
}, __classPrivateFieldGet(this, _Application_servicers, "f"), {
|
|
537
587
|
start: async (serverId, port, createExternalContext) => {
|
|
538
588
|
// Store `createExternalContext` function before listening
|
|
539
589
|
// so we don't attempt to serve any traffic and try and use
|
|
@@ -592,11 +642,14 @@ export class Application {
|
|
|
592
642
|
throw error;
|
|
593
643
|
}
|
|
594
644
|
}
|
|
595
|
-
}, initializeBearerToken, tokenVerifier), "f");
|
|
645
|
+
}, initializeBearerToken, tokenVerifier, __classPrivateFieldGet(this, _Application_libraries, "f").map(prepareLibrary)), "f");
|
|
596
646
|
}
|
|
597
647
|
get servicers() {
|
|
598
648
|
return __classPrivateFieldGet(this, _Application_servicers, "f");
|
|
599
649
|
}
|
|
650
|
+
get libraries() {
|
|
651
|
+
return __classPrivateFieldGet(this, _Application_libraries, "f");
|
|
652
|
+
}
|
|
600
653
|
get tokenVerifier() {
|
|
601
654
|
return __classPrivateFieldGet(this, _Application_tokenVerifier, "f");
|
|
602
655
|
}
|
|
@@ -612,13 +665,25 @@ export class Application {
|
|
|
612
665
|
please report this issue to the maintainers.`);
|
|
613
666
|
});
|
|
614
667
|
}
|
|
668
|
+
// Get all the TypeScript libraries and sort them by name.
|
|
669
|
+
const tsLibraries = __classPrivateFieldGet(this, _Application_libraries, "f")
|
|
670
|
+
.filter((library) => library.nativeLibraryModule === undefined)
|
|
671
|
+
.map((library) => library)
|
|
672
|
+
.sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0));
|
|
673
|
+
// Execute pre-run for TypeScript libraries.
|
|
674
|
+
// Python's NodeApplication responsible for running Python libraries.
|
|
675
|
+
for (const library of tsLibraries) {
|
|
676
|
+
if (library.preRun) {
|
|
677
|
+
library.preRun(this);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
615
680
|
return await reboot_native.Application_run(__classPrivateFieldGet(this, _Application_external, "f"));
|
|
616
681
|
}
|
|
617
682
|
get __external() {
|
|
618
683
|
return __classPrivateFieldGet(this, _Application_external, "f");
|
|
619
684
|
}
|
|
620
685
|
}
|
|
621
|
-
_Application_servicers = new WeakMap(), _Application_tokenVerifier = new WeakMap(), _Application_express = new WeakMap(), _Application_http = new WeakMap(), _Application_servers = new WeakMap(), _Application_createExternalContext = new WeakMap(), _Application_external = new WeakMap();
|
|
686
|
+
_Application_servicers = new WeakMap(), _Application_libraries = new WeakMap(), _Application_tokenVerifier = new WeakMap(), _Application_express = new WeakMap(), _Application_http = new WeakMap(), _Application_servers = new WeakMap(), _Application_createExternalContext = new WeakMap(), _Application_external = new WeakMap();
|
|
622
687
|
(function (Application) {
|
|
623
688
|
var _Http_express, _Http_createExternalContext;
|
|
624
689
|
class Http {
|
package/package.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@bufbuild/protoplugin": "1.10.1",
|
|
4
4
|
"@bufbuild/protoc-gen-es": "1.10.1",
|
|
5
|
-
"@reboot-dev/reboot-api": "0.
|
|
5
|
+
"@reboot-dev/reboot-api": "0.45.1",
|
|
6
6
|
"chalk": "^4.1.2",
|
|
7
7
|
"node-addon-api": "^7.0.0",
|
|
8
8
|
"node-gyp": ">=10.2.0",
|
|
9
|
-
"uuid": "
|
|
9
|
+
"uuid": "11.1.0",
|
|
10
10
|
"which-pm-runs": "^1.1.0",
|
|
11
11
|
"extensionless": "^1.9.9",
|
|
12
12
|
"esbuild": "^0.24.0",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
24
|
"name": "@reboot-dev/reboot",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.45.1",
|
|
26
26
|
"description": "npm package for Reboot",
|
|
27
27
|
"scripts": {
|
|
28
28
|
"preinstall": "node preinstall.cjs",
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
"typescript": "5.4.5",
|
|
36
36
|
"@types/express": "^5.0.1",
|
|
37
37
|
"@types/node": "20.11.5",
|
|
38
|
-
"@types/uuid": "^9.0.4",
|
|
39
38
|
"@types/express-serve-static-core": "^5.0.6"
|
|
40
39
|
},
|
|
41
40
|
"bin": {
|
package/reboot_native.cc
CHANGED
|
@@ -86,8 +86,7 @@ struct PythonNodeAdaptor {
|
|
|
86
86
|
dynamic_cast<const py::error_already_set*>(&e)) {
|
|
87
87
|
// This is a Python exception. Is it an `InputError`?
|
|
88
88
|
if (e_py->matches(
|
|
89
|
-
py::module::import("
|
|
90
|
-
.attr("InputError"))) {
|
|
89
|
+
py::module::import("reboot.aio.exceptions").attr("InputError"))) {
|
|
91
90
|
// This is an InputError, which means it reports a mistake in the input
|
|
92
91
|
// provided by the developer. We want to print _only_ the user-friendly
|
|
93
92
|
// error message in the exception, without intimidating stack traces.
|
|
@@ -128,7 +127,7 @@ struct PythonNodeAdaptor {
|
|
|
128
127
|
if (signal_fd) {
|
|
129
128
|
uint64_t one = 1;
|
|
130
129
|
// Writing to this file descriptor communicates to Python
|
|
131
|
-
// (see `public/
|
|
130
|
+
// (see `public/reboot/nodejs/python.py`) that
|
|
132
131
|
// `run_functions()` should be called (while holding the Python
|
|
133
132
|
// GIL). In `Initialize()` we've set up `run_functions()` to run
|
|
134
133
|
// the `python_functions` we've just added our callback to.
|
|
@@ -398,7 +397,7 @@ void PythonNodeAdaptor::Initialize(
|
|
|
398
397
|
py::initialize_interpreter();
|
|
399
398
|
|
|
400
399
|
try {
|
|
401
|
-
py::object module = py::module::import("
|
|
400
|
+
py::object module = py::module::import("reboot.nodejs.python");
|
|
402
401
|
|
|
403
402
|
module.attr("launch_subprocess_server") =
|
|
404
403
|
py::cpp_function([](py::str py_base64_args) {
|
|
@@ -858,7 +857,7 @@ Napi::Promise NodePromiseFromPythonTaskWithContext(
|
|
|
858
857
|
env,
|
|
859
858
|
std::move(name),
|
|
860
859
|
std::make_tuple(
|
|
861
|
-
std::string("
|
|
860
|
+
std::string("reboot.nodejs.python"),
|
|
862
861
|
std::string("create_task_with_context"),
|
|
863
862
|
py_context),
|
|
864
863
|
[js_context, // Ensures `py_context` remains valid.
|
|
@@ -963,7 +962,7 @@ py::object PythonFutureFromNodePromise(
|
|
|
963
962
|
})});
|
|
964
963
|
} catch (const std::exception& e) {
|
|
965
964
|
// NOTE: we're just catching exception here vs `Napi::Error`
|
|
966
|
-
// since all we care about is `what()` but we're making
|
|
965
|
+
// since all we care about is `what()` but we're making sue
|
|
967
966
|
// that we'll get all `Napi::Error` with this
|
|
968
967
|
// `static_assert`.
|
|
969
968
|
static_assert(
|
|
@@ -1045,7 +1044,7 @@ void ImportPy(const Napi::CallbackInfo& info) {
|
|
|
1045
1044
|
std::string base64_encoded_rbt_py = info[1].As<Napi::String>().Utf8Value();
|
|
1046
1045
|
|
|
1047
1046
|
RunCallbackOnPythonEventLoop([&module, &base64_encoded_rbt_py]() {
|
|
1048
|
-
py::module::import("
|
|
1047
|
+
py::module::import("reboot.nodejs.python")
|
|
1049
1048
|
.attr("import_py")(module, base64_encoded_rbt_py);
|
|
1050
1049
|
});
|
|
1051
1050
|
}
|
|
@@ -1081,7 +1080,7 @@ static const napi_type_tag reboot_aio_applications_Application =
|
|
|
1081
1080
|
|
|
1082
1081
|
|
|
1083
1082
|
static const napi_type_tag reboot_aio_external_ExternalContext =
|
|
1084
|
-
MakeTypeTag("
|
|
1083
|
+
MakeTypeTag("reboot.aio.external", "ExternalContext");
|
|
1085
1084
|
|
|
1086
1085
|
|
|
1087
1086
|
Napi::Value python3Path(const Napi::CallbackInfo& info) {
|
|
@@ -1268,8 +1267,8 @@ py::object make_py_authorizer(NapiSafeObjectReference js_authorizer) {
|
|
|
1268
1267
|
py::is_method(py::none()));
|
|
1269
1268
|
|
|
1270
1269
|
// Now define our subclass.
|
|
1271
|
-
py::object py_parent_class =
|
|
1272
|
-
|
|
1270
|
+
py::object py_parent_class =
|
|
1271
|
+
py::module::import("reboot.nodejs.python").attr("NodeAdaptorAuthorizer");
|
|
1273
1272
|
|
|
1274
1273
|
py::object py_parent_metaclass =
|
|
1275
1274
|
py::reinterpret_borrow<py::object>((PyObject*) &PyType_Type);
|
|
@@ -1531,12 +1530,203 @@ py::list make_py_servicers(
|
|
|
1531
1530
|
|
|
1532
1531
|
// Include memoize servicers by default!
|
|
1533
1532
|
py_servicers.attr("extend")(
|
|
1534
|
-
py::module::import("
|
|
1533
|
+
py::module::import("reboot.aio.memoize").attr("servicers")());
|
|
1535
1534
|
|
|
1536
1535
|
return py_servicers;
|
|
1537
1536
|
}
|
|
1538
1537
|
|
|
1539
1538
|
|
|
1539
|
+
struct TypeScriptLibraryDetails {
|
|
1540
|
+
std::string name;
|
|
1541
|
+
std::vector<std::shared_ptr<ServicerDetails>> js_servicers;
|
|
1542
|
+
std::vector<std::string> js_requirements;
|
|
1543
|
+
std::optional<NapiSafeFunctionReference> js_initialize;
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
struct PythonNativeLibraryDetails {
|
|
1547
|
+
std::string py_library_module;
|
|
1548
|
+
std::string py_library_function;
|
|
1549
|
+
std::optional<NapiSafeObjectReference> js_authorizer;
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
using LibraryDetails =
|
|
1553
|
+
std::variant<TypeScriptLibraryDetails, PythonNativeLibraryDetails>;
|
|
1554
|
+
std::vector<std::shared_ptr<LibraryDetails>> make_library_details(
|
|
1555
|
+
Napi::Env env,
|
|
1556
|
+
const Napi::Array& js_libraries) {
|
|
1557
|
+
std::vector<std::shared_ptr<LibraryDetails>> library_details;
|
|
1558
|
+
|
|
1559
|
+
for (auto&& [_, v] : js_libraries) {
|
|
1560
|
+
Napi::Object js_library = Napi::Value(v).As<Napi::Object>();
|
|
1561
|
+
|
|
1562
|
+
if (!js_library.Get("nativeLibraryModule").IsUndefined()) {
|
|
1563
|
+
std::string module =
|
|
1564
|
+
js_library.Get("nativeLibraryModule").As<Napi::String>().Utf8Value();
|
|
1565
|
+
std::string function = js_library.Get("nativeLibraryFunction")
|
|
1566
|
+
.As<Napi::String>()
|
|
1567
|
+
.Utf8Value();
|
|
1568
|
+
|
|
1569
|
+
// Get authorizer.
|
|
1570
|
+
std::optional<NapiSafeObjectReference> js_authorizer;
|
|
1571
|
+
if (!js_library.Get("authorizer").IsUndefined()) {
|
|
1572
|
+
js_authorizer = NapiSafeObjectReference(
|
|
1573
|
+
js_library.Get("authorizer").As<Napi::Object>());
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
library_details.push_back(
|
|
1577
|
+
std::make_shared<LibraryDetails>(
|
|
1578
|
+
PythonNativeLibraryDetails{module, function, js_authorizer}));
|
|
1579
|
+
} else if (
|
|
1580
|
+
!js_library.Get("name").IsUndefined()
|
|
1581
|
+
&& !js_library.Get("servicers").IsUndefined()) {
|
|
1582
|
+
std::string name = js_library.Get("name").As<Napi::String>().Utf8Value();
|
|
1583
|
+
|
|
1584
|
+
// Get servicers.
|
|
1585
|
+
Napi::Function js_servicers_func =
|
|
1586
|
+
js_library.Get("servicers").As<Napi::Function>();
|
|
1587
|
+
Napi::Value js_servicers_value = js_servicers_func.Call(js_library, {});
|
|
1588
|
+
Napi::Array js_servicers = js_servicers_value.As<Napi::Array>();
|
|
1589
|
+
auto servicer_details = make_servicer_details(env, js_servicers);
|
|
1590
|
+
|
|
1591
|
+
// Get requirements.
|
|
1592
|
+
std::vector<std::string> requirements;
|
|
1593
|
+
if (!js_library.Get("requirements").IsUndefined()) {
|
|
1594
|
+
Napi::Function js_requirements_func =
|
|
1595
|
+
js_library.Get("requirements").As<Napi::Function>();
|
|
1596
|
+
Napi::Value js_requirements_value =
|
|
1597
|
+
js_requirements_func.Call(js_library, {});
|
|
1598
|
+
Napi::Array js_requirements = js_requirements_value.As<Napi::Array>();
|
|
1599
|
+
for (auto&& [_, requirements_v] : js_requirements) {
|
|
1600
|
+
std::string requirement =
|
|
1601
|
+
Napi::Value(requirements_v).As<Napi::String>().Utf8Value();
|
|
1602
|
+
requirements.push_back(requirement);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
std::optional<NapiSafeFunctionReference> js_initialize;
|
|
1607
|
+
if (!js_library.Get("initialize").IsUndefined()) {
|
|
1608
|
+
js_initialize = NapiSafeFunctionReference(
|
|
1609
|
+
js_library.Get("initialize").As<Napi::Function>());
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
library_details.push_back(
|
|
1613
|
+
std::make_shared<LibraryDetails>(TypeScriptLibraryDetails{
|
|
1614
|
+
name,
|
|
1615
|
+
servicer_details,
|
|
1616
|
+
requirements,
|
|
1617
|
+
js_initialize}));
|
|
1618
|
+
} else {
|
|
1619
|
+
Napi::Error::New(env, "Unexpected `library` type.")
|
|
1620
|
+
.ThrowAsJavaScriptException();
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
return library_details;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
py::object make_py_typescript_library(
|
|
1628
|
+
TypeScriptLibraryDetails& details,
|
|
1629
|
+
NapiSafeFunctionReference js_from_native_external) {
|
|
1630
|
+
// Construct a subclass of Library.
|
|
1631
|
+
//
|
|
1632
|
+
// What we do below is the recommended way to do that, see:
|
|
1633
|
+
// https://github.com/pybind/pybind11/issues/1193
|
|
1634
|
+
|
|
1635
|
+
// First create all of the attributes that we'll want this
|
|
1636
|
+
// subclass to have.
|
|
1637
|
+
py::dict attributes;
|
|
1638
|
+
attributes["name"] = details.name;
|
|
1639
|
+
attributes["_servicers"] = make_py_servicers(details.js_servicers);
|
|
1640
|
+
attributes["_requirements"] = details.js_requirements;
|
|
1641
|
+
attributes["_initialize"] = py::none();
|
|
1642
|
+
|
|
1643
|
+
if (details.js_initialize.has_value()) {
|
|
1644
|
+
NapiSafeFunctionReference js_initialize = details.js_initialize.value();
|
|
1645
|
+
|
|
1646
|
+
attributes["_initialize"] = py::cpp_function(
|
|
1647
|
+
[js_initialize = std::move(js_initialize),
|
|
1648
|
+
js_from_native_external /* Need a copy because it is shared. ??? */](
|
|
1649
|
+
py::object py_context) mutable {
|
|
1650
|
+
return PythonFutureFromNodePromise(
|
|
1651
|
+
[js_initialize,
|
|
1652
|
+
js_from_native_external, // NOTE: need a _copy_ of
|
|
1653
|
+
// both `js_initialize`
|
|
1654
|
+
// and
|
|
1655
|
+
// `js_from_native_external`
|
|
1656
|
+
// here since
|
|
1657
|
+
// `py_initialize` may be
|
|
1658
|
+
// called more than once!
|
|
1659
|
+
py_context = new py::object(py_context)](Napi::Env env) mutable {
|
|
1660
|
+
Napi::External<py::object> js_external_context =
|
|
1661
|
+
make_napi_external(
|
|
1662
|
+
env,
|
|
1663
|
+
py_context,
|
|
1664
|
+
&reboot_aio_external_ExternalContext);
|
|
1665
|
+
Napi::Object js_context =
|
|
1666
|
+
js_from_native_external.Value(env)
|
|
1667
|
+
.Call(
|
|
1668
|
+
env.Global(),
|
|
1669
|
+
{js_external_context,
|
|
1670
|
+
Napi::String::New(env, "initialize")})
|
|
1671
|
+
.As<Napi::Object>();
|
|
1672
|
+
|
|
1673
|
+
return js_initialize.Value(env)
|
|
1674
|
+
.Call(env.Global(), {js_context})
|
|
1675
|
+
.As<Napi::Object>();
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Define the subclass.
|
|
1681
|
+
py::object py_parent_class =
|
|
1682
|
+
py::module::import("reboot.aio.applications").attr("NodeAdaptorLibrary");
|
|
1683
|
+
|
|
1684
|
+
py::object py_parent_metaclass =
|
|
1685
|
+
py::reinterpret_borrow<py::object>((PyObject*) &PyType_Type);
|
|
1686
|
+
|
|
1687
|
+
py::object py_library = py_parent_metaclass(
|
|
1688
|
+
"_NodeAdaptorLibrary",
|
|
1689
|
+
py::make_tuple(py_parent_class),
|
|
1690
|
+
attributes);
|
|
1691
|
+
|
|
1692
|
+
return py_library();
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// NOTE: must be called from within _Python_.
|
|
1696
|
+
py::list make_py_libraries(
|
|
1697
|
+
const std::vector<std::shared_ptr<LibraryDetails>>& library_details,
|
|
1698
|
+
NapiSafeFunctionReference js_from_native_external) {
|
|
1699
|
+
py::list py_libraries;
|
|
1700
|
+
for (const auto& details : library_details) {
|
|
1701
|
+
if (auto typescript_details =
|
|
1702
|
+
std::get_if<TypeScriptLibraryDetails>(details.get())) {
|
|
1703
|
+
py_libraries.append(make_py_typescript_library(
|
|
1704
|
+
*typescript_details,
|
|
1705
|
+
js_from_native_external));
|
|
1706
|
+
} else if (
|
|
1707
|
+
auto python_details =
|
|
1708
|
+
std::get_if<PythonNativeLibraryDetails>(details.get())) {
|
|
1709
|
+
// Call the library() function with `authorizer` if one is provided.
|
|
1710
|
+
py::object py_library;
|
|
1711
|
+
if (python_details->js_authorizer.has_value()) {
|
|
1712
|
+
py_library =
|
|
1713
|
+
py::module::import(python_details->py_library_module.c_str())
|
|
1714
|
+
.attr(python_details->py_library_function.c_str())(
|
|
1715
|
+
"authorizer"_a =
|
|
1716
|
+
make_py_authorizer(*python_details->js_authorizer));
|
|
1717
|
+
} else {
|
|
1718
|
+
py_library =
|
|
1719
|
+
py::module::import(python_details->py_library_module.c_str())
|
|
1720
|
+
.attr(python_details->py_library_function.c_str())();
|
|
1721
|
+
}
|
|
1722
|
+
py_libraries.append(py_library);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
return py_libraries;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
|
|
1540
1730
|
// NOTE: must be called from within _Python_.
|
|
1541
1731
|
py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
|
|
1542
1732
|
// Construct a subclass of TokenVerifier.
|
|
@@ -1615,7 +1805,7 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
|
|
|
1615
1805
|
py::arg("bytes_call"),
|
|
1616
1806
|
py::is_method(py::none()));
|
|
1617
1807
|
|
|
1618
|
-
py::object py_parent_class = py::module::import("
|
|
1808
|
+
py::object py_parent_class = py::module::import("reboot.nodejs.python")
|
|
1619
1809
|
.attr("NodeAdaptorTokenVerifier");
|
|
1620
1810
|
|
|
1621
1811
|
py::object py_parent_metaclass =
|
|
@@ -1646,7 +1836,7 @@ Napi::Value Reboot_up(const Napi::CallbackInfo& info) {
|
|
|
1646
1836
|
|
|
1647
1837
|
py::object* py_application = js_external_application.Value(info.Env()).Data();
|
|
1648
1838
|
|
|
1649
|
-
bool local_envoy
|
|
1839
|
+
std::optional<bool> local_envoy;
|
|
1650
1840
|
if (!info[2].IsUndefined()) {
|
|
1651
1841
|
local_envoy = info[2].As<Napi::Boolean>();
|
|
1652
1842
|
}
|
|
@@ -1666,15 +1856,13 @@ Napi::Value Reboot_up(const Napi::CallbackInfo& info) {
|
|
|
1666
1856
|
py_application,
|
|
1667
1857
|
local_envoy,
|
|
1668
1858
|
local_envoy_port]() {
|
|
1859
|
+
py::object py_local_envoy = py::none();
|
|
1860
|
+
if (local_envoy.has_value()) {
|
|
1861
|
+
py_local_envoy = py::bool_(*local_envoy);
|
|
1862
|
+
}
|
|
1669
1863
|
return py_reboot->attr("up")(
|
|
1670
1864
|
py_application,
|
|
1671
|
-
|
|
1672
|
-
// for `rbt dev` and `rbt serve` we do not support
|
|
1673
|
-
// them for tests because we don't have a way to
|
|
1674
|
-
// clone a process like we do with multiprocessing
|
|
1675
|
-
// in Python.
|
|
1676
|
-
"in_process"_a = true,
|
|
1677
|
-
"local_envoy"_a = local_envoy,
|
|
1865
|
+
"local_envoy"_a = py_local_envoy,
|
|
1678
1866
|
"local_envoy_port"_a = local_envoy_port);
|
|
1679
1867
|
},
|
|
1680
1868
|
[](py::object py_revision) {
|
|
@@ -1759,8 +1947,8 @@ Napi::Value Reboot_stop(const Napi::CallbackInfo& info) {
|
|
|
1759
1947
|
return js_promise;
|
|
1760
1948
|
}
|
|
1761
1949
|
|
|
1762
|
-
// NOTE: We block on a promise here, so this method should not be called
|
|
1763
|
-
// of tests.
|
|
1950
|
+
// NOTE: We block on a promise here, so this method should not be called
|
|
1951
|
+
// outside of tests.
|
|
1764
1952
|
Napi::Value Reboot_url(const Napi::CallbackInfo& info) {
|
|
1765
1953
|
// NOTE: we immediately get a safe reference to the `Napi::External`
|
|
1766
1954
|
// so that Node will not garbage collect it and the `py::object*` we
|
|
@@ -1912,7 +2100,7 @@ Napi::Value Task_await(const Napi::CallbackInfo& info) {
|
|
|
1912
2100
|
|
|
1913
2101
|
return NodePromiseFromPythonTaskWithContext(
|
|
1914
2102
|
info.Env(),
|
|
1915
|
-
"
|
|
2103
|
+
"reboot.nodejs.python.task_await(\"" + state_name + "\", \"" + method
|
|
1916
2104
|
+ "\", ...) in nodejs",
|
|
1917
2105
|
js_external_context,
|
|
1918
2106
|
[rbt_module = std::move(rbt_module),
|
|
@@ -1921,7 +2109,7 @@ Napi::Value Task_await(const Napi::CallbackInfo& info) {
|
|
|
1921
2109
|
js_external_context, // Ensures `py_context` remains valid.
|
|
1922
2110
|
py_context,
|
|
1923
2111
|
json_task_id]() {
|
|
1924
|
-
return py::module::import("
|
|
2112
|
+
return py::module::import("reboot.nodejs.python")
|
|
1925
2113
|
.attr("task_await")(
|
|
1926
2114
|
py_context,
|
|
1927
2115
|
py::module::import(rbt_module.c_str()).attr(state_name.c_str()),
|
|
@@ -1969,7 +2157,7 @@ Napi::Value ExternalContext_constructor(const Napi::CallbackInfo& info) {
|
|
|
1969
2157
|
&idempotency_seed,
|
|
1970
2158
|
&idempotency_required,
|
|
1971
2159
|
&idempotency_required_reason]() {
|
|
1972
|
-
py::object py_external = py::module::import("
|
|
2160
|
+
py::object py_external = py::module::import("reboot.aio.external");
|
|
1973
2161
|
|
|
1974
2162
|
auto convert_str =
|
|
1975
2163
|
[](const std::optional<std::string>& optional) -> py::object {
|
|
@@ -2003,6 +2191,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2003
2191
|
NapiSafeFunctionReference(info[0].As<Napi::Function>());
|
|
2004
2192
|
|
|
2005
2193
|
Napi::Array js_servicers = info[1].As<Napi::Array>();
|
|
2194
|
+
auto servicer_details = make_servicer_details(info.Env(), js_servicers);
|
|
2006
2195
|
|
|
2007
2196
|
Napi::Object js_web_framework = info[2].As<Napi::Object>();
|
|
2008
2197
|
|
|
@@ -2012,8 +2201,6 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2012
2201
|
auto js_web_framework_stop = NapiSafeFunctionReference(
|
|
2013
2202
|
js_web_framework.Get("stop").As<Napi::Function>());
|
|
2014
2203
|
|
|
2015
|
-
auto servicer_details = make_servicer_details(info.Env(), js_servicers);
|
|
2016
|
-
|
|
2017
2204
|
auto js_initialize = NapiSafeFunctionReference(info[3].As<Napi::Function>());
|
|
2018
2205
|
|
|
2019
2206
|
std::optional<std::string> initialize_bearer_token;
|
|
@@ -2026,6 +2213,9 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2026
2213
|
js_token_verifier = NapiSafeReference(info[5].As<Napi::Object>());
|
|
2027
2214
|
}
|
|
2028
2215
|
|
|
2216
|
+
Napi::Array js_libraries = info[6].As<Napi::Array>();
|
|
2217
|
+
auto library_details = make_library_details(info.Env(), js_libraries);
|
|
2218
|
+
|
|
2029
2219
|
py::object* py_application = RunCallbackOnPythonEventLoop(
|
|
2030
2220
|
[servicer_details = std::move(servicer_details),
|
|
2031
2221
|
js_web_framework_start = std::move(js_web_framework_start),
|
|
@@ -2033,8 +2223,11 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2033
2223
|
initialize_bearer_token = std::move(initialize_bearer_token),
|
|
2034
2224
|
js_initialize = std::move(js_initialize),
|
|
2035
2225
|
js_token_verifier,
|
|
2036
|
-
js_from_native_external = std::move(js_from_native_external)
|
|
2226
|
+
js_from_native_external = std::move(js_from_native_external),
|
|
2227
|
+
library_details = std::move(library_details)]() {
|
|
2037
2228
|
py::list py_servicers = make_py_servicers(servicer_details);
|
|
2229
|
+
py::list py_libraries =
|
|
2230
|
+
make_py_libraries(library_details, js_from_native_external);
|
|
2038
2231
|
|
|
2039
2232
|
py::object py_web_framework_start = py::cpp_function(
|
|
2040
2233
|
[js_web_framework_start = std::move(js_web_framework_start),
|
|
@@ -2101,7 +2294,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2101
2294
|
|
|
2102
2295
|
return new py::object(
|
|
2103
2296
|
py::module::import(
|
|
2104
|
-
"
|
|
2297
|
+
"reboot.aio.external")
|
|
2105
2298
|
.attr("ExternalContext")(
|
|
2106
2299
|
"name"_a = py::str(name),
|
|
2107
2300
|
"channel_manager"_a =
|
|
@@ -2212,6 +2405,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2212
2405
|
py::module::import("reboot.aio.applications")
|
|
2213
2406
|
.attr("NodeApplication")(
|
|
2214
2407
|
"servicers"_a = py_servicers,
|
|
2408
|
+
"libraries"_a = py_libraries,
|
|
2215
2409
|
"web_framework_start"_a = py_web_framework_start,
|
|
2216
2410
|
"web_framework_stop"_a = py_web_framework_stop,
|
|
2217
2411
|
"initialize"_a = py_initialize,
|
|
@@ -2241,7 +2435,7 @@ Napi::Value Application_run(const Napi::CallbackInfo& info) {
|
|
|
2241
2435
|
Napi::Promise js_promise = NodePromiseFromPythonTask(
|
|
2242
2436
|
info.Env(),
|
|
2243
2437
|
"Application.run() in nodejs",
|
|
2244
|
-
{"
|
|
2438
|
+
{"reboot.nodejs.python", "create_task"},
|
|
2245
2439
|
[js_external_application, // Ensures `py_application` remains valid.
|
|
2246
2440
|
py_application]() { return py_application->attr("run")(); });
|
|
2247
2441
|
|
|
@@ -2295,10 +2489,10 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
|
|
|
2295
2489
|
if (js_alias.IsString()) {
|
|
2296
2490
|
alias = js_alias.As<Napi::String>().Utf8Value();
|
|
2297
2491
|
}
|
|
2298
|
-
auto
|
|
2299
|
-
std::optional<bool>
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2492
|
+
auto js_per_iteration = idempotency_options.Get("perIteration");
|
|
2493
|
+
std::optional<bool> per_iteration;
|
|
2494
|
+
if (js_per_iteration.IsBoolean()) {
|
|
2495
|
+
per_iteration = js_per_iteration.As<Napi::Boolean>();
|
|
2302
2496
|
}
|
|
2303
2497
|
|
|
2304
2498
|
return NodePromiseFromPythonCallback(
|
|
@@ -2310,11 +2504,11 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
|
|
|
2310
2504
|
method = std::move(method),
|
|
2311
2505
|
key = std::move(key),
|
|
2312
2506
|
alias = std::move(alias),
|
|
2313
|
-
|
|
2507
|
+
per_iteration = std::move(per_iteration)]() {
|
|
2314
2508
|
// Need to use `call_with_context` to ensure that we have
|
|
2315
2509
|
// `py_context` as a valid asyncio context variable.
|
|
2316
2510
|
py::object py_idempotency =
|
|
2317
|
-
py::module::import("
|
|
2511
|
+
py::module::import("reboot.nodejs.python")
|
|
2318
2512
|
.attr("call_with_context")(
|
|
2319
2513
|
py::cpp_function([&]() {
|
|
2320
2514
|
py::object py_key = py::none();
|
|
@@ -2325,16 +2519,17 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
|
|
|
2325
2519
|
if (alias.has_value()) {
|
|
2326
2520
|
py_alias = py::cast(*alias);
|
|
2327
2521
|
}
|
|
2328
|
-
py::object
|
|
2329
|
-
if (
|
|
2330
|
-
|
|
2522
|
+
py::object py_how = py::none();
|
|
2523
|
+
if (per_iteration.has_value() && *per_iteration) {
|
|
2524
|
+
py_how = py::module::import("reboot.aio.idempotency")
|
|
2525
|
+
.attr("PER_ITERATION");
|
|
2331
2526
|
}
|
|
2332
|
-
return py::module::import("
|
|
2527
|
+
return py::module::import("reboot.aio.contexts")
|
|
2333
2528
|
.attr("Context")
|
|
2334
2529
|
.attr("idempotency")(
|
|
2335
2530
|
"key"_a = py_key,
|
|
2336
2531
|
"alias"_a = py_alias,
|
|
2337
|
-
"
|
|
2532
|
+
"how"_a = py_how);
|
|
2338
2533
|
}),
|
|
2339
2534
|
py_context);
|
|
2340
2535
|
|
|
@@ -2390,7 +2585,7 @@ Napi::Value WorkflowContext_loop(const Napi::CallbackInfo& info) {
|
|
|
2390
2585
|
[js_external_context, // Ensures `py_context` remains valid.
|
|
2391
2586
|
py_context,
|
|
2392
2587
|
alias = std::move(alias)]() {
|
|
2393
|
-
return py::module::import("
|
|
2588
|
+
return py::module::import("reboot.nodejs.python")
|
|
2394
2589
|
.attr("loop")(py_context, alias);
|
|
2395
2590
|
},
|
|
2396
2591
|
[](py::object py_iterate) { return new py::object(py_iterate); },
|
|
@@ -2468,7 +2663,7 @@ Napi::Value retry_reactively_until(const Napi::CallbackInfo& info) {
|
|
|
2468
2663
|
});
|
|
2469
2664
|
});
|
|
2470
2665
|
|
|
2471
|
-
return py::module::import("
|
|
2666
|
+
return py::module::import("reboot.aio.contexts")
|
|
2472
2667
|
.attr("retry_reactively_until")(py_context, py_condition);
|
|
2473
2668
|
});
|
|
2474
2669
|
}
|
|
@@ -2520,7 +2715,7 @@ Napi::Value memoize(const Napi::CallbackInfo& info) {
|
|
|
2520
2715
|
});
|
|
2521
2716
|
});
|
|
2522
2717
|
|
|
2523
|
-
return py::module::import("
|
|
2718
|
+
return py::module::import("reboot.aio.memoize")
|
|
2524
2719
|
.attr("memoize")(
|
|
2525
2720
|
py::make_tuple(alias, how),
|
|
2526
2721
|
py_context,
|
package/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const REBOOT_VERSION = "0.
|
|
1
|
+
export declare const REBOOT_VERSION = "0.45.1";
|
package/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const REBOOT_VERSION = "0.
|
|
1
|
+
export const REBOOT_VERSION = "0.45.1";
|
package/zod-to-proto.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import { toCamelCase, toPascalCase, toSnakeCase, typeSchema, ZOD_ERROR_NAMES, } from "@reboot-dev/reboot-api";
|
|
2
|
+
import { ALLOWED_DEFAULT_BY_CONSTRUCTOR_NAME, toCamelCase, toPascalCase, toSnakeCase, typeSchema, ZOD_ERROR_NAMES, } from "@reboot-dev/reboot-api";
|
|
3
3
|
import { strict as assert } from "assert";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { mkdtemp } from "fs/promises";
|
|
@@ -56,7 +56,7 @@ const generate = (proto, { schema, path, name, state = false, }) => {
|
|
|
56
56
|
const { tag } = meta;
|
|
57
57
|
if (tags.has(tag)) {
|
|
58
58
|
// TODO: give "path" to this property.
|
|
59
|
-
console.error(chalk.stderr.bold.red(`Trying to use tag ${tag} with property '${key}' already used by '${tags.get(tag)}'`));
|
|
59
|
+
console.error(chalk.stderr.bold.red(`Trying to use tag '${tag}' with property '${key}' already used by '${tags.get(tag)}'`));
|
|
60
60
|
process.exit(-1);
|
|
61
61
|
}
|
|
62
62
|
tags.set(tag, key);
|
|
@@ -79,7 +79,8 @@ const generate = (proto, { schema, path, name, state = false, }) => {
|
|
|
79
79
|
`primitive type (e.g., string, number, boolean) ` +
|
|
80
80
|
`which is immutable or pass a function that creates ` +
|
|
81
81
|
`a new ${Array.isArray(firstDefaultValue) ? "array" : "object"} ` +
|
|
82
|
-
`each time
|
|
82
|
+
`each time (for example \`reboot_api.EMPTY_ARRAY\` or ` +
|
|
83
|
+
`\`reboot_api.EMPTY_RECORD\`).`));
|
|
83
84
|
process.exit(-1);
|
|
84
85
|
}
|
|
85
86
|
// Otherwise, if objects are different, that means that
|
|
@@ -114,7 +115,18 @@ const generate = (proto, { schema, path, name, state = false, }) => {
|
|
|
114
115
|
console.error(chalk.stderr.bold.red(`Unexpected literal '${literal}' for property '${key}'; only 'string' literals are currently supported`));
|
|
115
116
|
process.exit(-1);
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
+
// According to Protobuf `enum` rules:
|
|
119
|
+
// `enum` values use C++ scoping rules, meaning that
|
|
120
|
+
// `enum` values are siblings of their type, not
|
|
121
|
+
// children of it.
|
|
122
|
+
// That means we need to prefix the `enum` values
|
|
123
|
+
// with the `enum` type name to avoid name conflicts.
|
|
124
|
+
// It is safe here, since we preserve the original
|
|
125
|
+
// order of the literals and during the conversion
|
|
126
|
+
// from Pydantic model to Protobuf and back we
|
|
127
|
+
// operate with the indexes of the literals, not
|
|
128
|
+
// their names.
|
|
129
|
+
proto.write(`${typeName}_${literal} = ${i++};`);
|
|
118
130
|
}
|
|
119
131
|
proto.write(`}`);
|
|
120
132
|
proto.write(`optional ${typeName} ${field} = ${tag};`);
|
|
@@ -390,7 +402,7 @@ const generate = (proto, { schema, path, name, state = false, }) => {
|
|
|
390
402
|
}
|
|
391
403
|
const { tag } = meta;
|
|
392
404
|
if (tags.has(tag)) {
|
|
393
|
-
console.error(chalk.stderr.bold.red(`Trying to use already used tag ${tag} in discriminated union`));
|
|
405
|
+
console.error(chalk.stderr.bold.red(`Trying to use already used tag '${tag}' in discriminated union`));
|
|
394
406
|
process.exit(-1);
|
|
395
407
|
}
|
|
396
408
|
tags.set(tag, [toSnakeCase(literal), typeName]);
|
|
@@ -413,6 +425,121 @@ const generate = (proto, { schema, path, name, state = false, }) => {
|
|
|
413
425
|
throw new Error(`Unexpected type '${schema._zod.def.type}'`);
|
|
414
426
|
}
|
|
415
427
|
};
|
|
428
|
+
const validateProperDefaultSpecified = (schema, path) => {
|
|
429
|
+
if (schema instanceof z.ZodObject) {
|
|
430
|
+
const shape = schema._zod.def.shape;
|
|
431
|
+
for (const key in shape) {
|
|
432
|
+
validateProperDefaultSpecified(shape[key], `${path}.${key}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else if (schema instanceof z.ZodRecord) {
|
|
436
|
+
validateProperDefaultSpecified(schema.valueType, `${path}.[value]`);
|
|
437
|
+
}
|
|
438
|
+
else if (schema instanceof z.ZodArray) {
|
|
439
|
+
validateProperDefaultSpecified(schema.element, `${path}.[item]`);
|
|
440
|
+
}
|
|
441
|
+
else if (schema instanceof z.ZodDiscriminatedUnion) {
|
|
442
|
+
for (const option of schema.options) {
|
|
443
|
+
// NOTE: The path in the error will be something like:
|
|
444
|
+
// `api.typeName.methods.methodName.errors.[object Object].field`.
|
|
445
|
+
validateProperDefaultSpecified(option, `${path}.${option}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (schema instanceof z.ZodDefault) {
|
|
449
|
+
const innerType = schema._zod.def.innerType;
|
|
450
|
+
const isOptional = innerType instanceof z.ZodOptional;
|
|
451
|
+
const isObject = innerType instanceof z.ZodObject;
|
|
452
|
+
const defaultValue = schema._zod.def.defaultValue;
|
|
453
|
+
// TODO: Write the document about why it is challenging
|
|
454
|
+
// to have default values in the distributed systems
|
|
455
|
+
// and attach the link to the error message.
|
|
456
|
+
if (innerType instanceof z.ZodDiscriminatedUnion) {
|
|
457
|
+
// Discriminated unions cannot have default values,
|
|
458
|
+
// because its options will be always different
|
|
459
|
+
// `z.object` types and we don't support that currently.
|
|
460
|
+
console.error(chalk.stderr.bold.red(`'${path}' is a discriminated union type and cannot have a ` +
|
|
461
|
+
`\`default\` value.`));
|
|
462
|
+
process.exit(-1);
|
|
463
|
+
}
|
|
464
|
+
else if (isObject && !isOptional) {
|
|
465
|
+
console.error(chalk.stderr.bold.red(`'${path}' is a non-optional object type and cannot have a ` +
|
|
466
|
+
`\`default\` value. Use \`optional()\` for object types with empty default.`));
|
|
467
|
+
process.exit(-1);
|
|
468
|
+
}
|
|
469
|
+
else if (defaultValue === undefined && !isOptional) {
|
|
470
|
+
console.error(chalk.stderr.bold.red(`'${path}' is a non-optional type and cannot have an ` +
|
|
471
|
+
`\`undefined\` default value. Change the \`default\` or make the ` +
|
|
472
|
+
`field \`optional()\`.`));
|
|
473
|
+
process.exit(-1);
|
|
474
|
+
}
|
|
475
|
+
else if (defaultValue !== undefined && isOptional) {
|
|
476
|
+
console.error(chalk.stderr.bold.red(`'${path}' is an \`optional\` type and can only have ` +
|
|
477
|
+
`\`undefined\` default value.`));
|
|
478
|
+
process.exit(-1);
|
|
479
|
+
}
|
|
480
|
+
else if (isOptional) {
|
|
481
|
+
// If the field is `optional` and uses `default=undefined`,
|
|
482
|
+
// it is valid, check the inner type recursively.
|
|
483
|
+
validateProperDefaultSpecified(innerType, path);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const isEmptyArray = innerType instanceof z.ZodArray &&
|
|
487
|
+
Array.isArray(defaultValue) &&
|
|
488
|
+
defaultValue.length === 0;
|
|
489
|
+
const isEmptyRecord = innerType instanceof z.ZodRecord &&
|
|
490
|
+
typeof defaultValue === "object" &&
|
|
491
|
+
defaultValue !== null &&
|
|
492
|
+
Object.keys(defaultValue).length === 0;
|
|
493
|
+
const innerTypeConstructorName = innerType.constructor.name;
|
|
494
|
+
const allowedDefault =
|
|
495
|
+
// Return `undefined` if not found.
|
|
496
|
+
ALLOWED_DEFAULT_BY_CONSTRUCTOR_NAME[innerTypeConstructorName];
|
|
497
|
+
if (innerType instanceof z.ZodArray) {
|
|
498
|
+
if (!isEmptyArray) {
|
|
499
|
+
console.error(chalk.stderr.bold.red(`'${path}' is an \`array\` with an unsupported default value. ` +
|
|
500
|
+
`Only empty default value is supported. Use ` +
|
|
501
|
+
`\`reboot_api.EMPTY_ARRAY\` to safely specify an empty array default.`));
|
|
502
|
+
process.exit(-1);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if (innerType instanceof z.ZodRecord) {
|
|
506
|
+
if (!isEmptyRecord) {
|
|
507
|
+
console.error(chalk.stderr.bold.red(`'${path}' is a \`record\` with an unsupported default value. ` +
|
|
508
|
+
`Only empty default value is supported. Use ` +
|
|
509
|
+
`\`reboot_api.EMPTY_RECORD\` to safely specify an empty record default.`));
|
|
510
|
+
process.exit(-1);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (innerType instanceof z.ZodLiteral) {
|
|
514
|
+
// For Literal types, the default value must be the first literal
|
|
515
|
+
// value in the list (according to how Protobuf `enum`s work).
|
|
516
|
+
const firstLiteralValue = innerType._zod.def.values[0];
|
|
517
|
+
if (defaultValue !== firstLiteralValue) {
|
|
518
|
+
console.error(chalk.stderr.bold.red(`'${path}' is a \`literal\` with an unsupported default value. ` +
|
|
519
|
+
`Only the first literal value \`${firstLiteralValue}\` is ` +
|
|
520
|
+
`supported as the default.`));
|
|
521
|
+
process.exit(-1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else if (allowedDefault === undefined) {
|
|
525
|
+
console.error(chalk.stderr.bold.red(`'${path}' uses \`default\` which is not supported for type ` +
|
|
526
|
+
`\`${innerTypeConstructorName}\`. Only ` +
|
|
527
|
+
`${Object.keys(ALLOWED_DEFAULT_BY_CONSTRUCTOR_NAME)
|
|
528
|
+
.map((name) => `\`${name}\``)
|
|
529
|
+
.join(", ")}` +
|
|
530
|
+
`, \`ZodArray\`, \`ZodRecord\`, \`ZodLiteral\`, and \`optional\` ` +
|
|
531
|
+
`types can have a \`default\` currently.`));
|
|
532
|
+
process.exit(-1);
|
|
533
|
+
}
|
|
534
|
+
else if (defaultValue !== allowedDefault) {
|
|
535
|
+
console.error(chalk.stderr.bold.red(`'${path}' has an unsupported default value. ` +
|
|
536
|
+
`Supported default value for type \`${innerTypeConstructorName}\` is ` +
|
|
537
|
+
`\`${JSON.stringify(allowedDefault)}\`.`));
|
|
538
|
+
process.exit(-1);
|
|
539
|
+
}
|
|
540
|
+
validateProperDefaultSpecified(innerType, path);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
416
543
|
const main = async () => {
|
|
417
544
|
// We generate the `.proto` files in a temporary directory.
|
|
418
545
|
const generatedProtosDirectory = await mkdtemp(path.join(os.tmpdir(), "protos-"));
|
|
@@ -490,6 +617,7 @@ const main = async () => {
|
|
|
490
617
|
// It is safe to do so, because right before that we call
|
|
491
618
|
// 'safeParse()', which ensures that the type is correct.
|
|
492
619
|
const type = result.data;
|
|
620
|
+
validateProperDefaultSpecified(type.state instanceof z.ZodObject ? type.state : z.object(type.state), `api.${typeName}.state`);
|
|
493
621
|
generate(proto, {
|
|
494
622
|
schema: type.state instanceof z.ZodObject ? type.state : z.object(type.state),
|
|
495
623
|
path: `api.${typeName}.state`,
|
|
@@ -500,6 +628,7 @@ const main = async () => {
|
|
|
500
628
|
for (const methodName in type.methods) {
|
|
501
629
|
// TODO: ensure `methodName` is PascalCase.
|
|
502
630
|
const { request, response } = type.methods[methodName];
|
|
631
|
+
validateProperDefaultSpecified(request instanceof z.ZodObject ? request : z.object(request), `api.${typeName}.methods.${methodName}.request`);
|
|
503
632
|
const requestTypeName = `${typeName}${toPascalCase(methodName)}Request`;
|
|
504
633
|
generate(proto, {
|
|
505
634
|
schema: request instanceof z.ZodObject ? request : z.object(request),
|
|
@@ -509,6 +638,7 @@ const main = async () => {
|
|
|
509
638
|
if (response instanceof z.ZodVoid) {
|
|
510
639
|
continue;
|
|
511
640
|
}
|
|
641
|
+
validateProperDefaultSpecified(response instanceof z.ZodObject ? response : z.object(response), `api.${typeName}.methods.${methodName}.response`);
|
|
512
642
|
const responseTypeName = `${typeName}${toPascalCase(methodName)}Response`;
|
|
513
643
|
generate(proto, {
|
|
514
644
|
schema: response instanceof z.ZodObject ? response : z.object(response),
|
|
@@ -543,6 +673,7 @@ const main = async () => {
|
|
|
543
673
|
const errors = method.errors instanceof z.ZodDiscriminatedUnion
|
|
544
674
|
? method.errors
|
|
545
675
|
: z.discriminatedUnion("type", method.errors);
|
|
676
|
+
validateProperDefaultSpecified(errors, `api.${typeName}.methods.${methodName}.errors`);
|
|
546
677
|
const path = `api.${typeName}.methods.${methodName}.errors`;
|
|
547
678
|
const name = `${typeName}${toPascalCase(methodName)}Errors`;
|
|
548
679
|
errorsToGenerate.push(() => {
|