@reboot-dev/reboot 0.44.0 → 0.45.2
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 +233 -42
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/zod-to-proto.js +124 -4
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.2",
|
|
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.2",
|
|
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 =
|
|
@@ -1672,12 +1862,6 @@ Napi::Value Reboot_up(const Napi::CallbackInfo& info) {
|
|
|
1672
1862
|
}
|
|
1673
1863
|
return py_reboot->attr("up")(
|
|
1674
1864
|
py_application,
|
|
1675
|
-
// NOTE: while we support subprocess servers
|
|
1676
|
-
// for `rbt dev` and `rbt serve` we do not support
|
|
1677
|
-
// them for tests because we don't have a way to
|
|
1678
|
-
// clone a process like we do with multiprocessing
|
|
1679
|
-
// in Python.
|
|
1680
|
-
"in_process"_a = true,
|
|
1681
1865
|
"local_envoy"_a = py_local_envoy,
|
|
1682
1866
|
"local_envoy_port"_a = local_envoy_port);
|
|
1683
1867
|
},
|
|
@@ -1763,8 +1947,8 @@ Napi::Value Reboot_stop(const Napi::CallbackInfo& info) {
|
|
|
1763
1947
|
return js_promise;
|
|
1764
1948
|
}
|
|
1765
1949
|
|
|
1766
|
-
// NOTE: We block on a promise here, so this method should not be called
|
|
1767
|
-
// of tests.
|
|
1950
|
+
// NOTE: We block on a promise here, so this method should not be called
|
|
1951
|
+
// outside of tests.
|
|
1768
1952
|
Napi::Value Reboot_url(const Napi::CallbackInfo& info) {
|
|
1769
1953
|
// NOTE: we immediately get a safe reference to the `Napi::External`
|
|
1770
1954
|
// so that Node will not garbage collect it and the `py::object*` we
|
|
@@ -1916,7 +2100,7 @@ Napi::Value Task_await(const Napi::CallbackInfo& info) {
|
|
|
1916
2100
|
|
|
1917
2101
|
return NodePromiseFromPythonTaskWithContext(
|
|
1918
2102
|
info.Env(),
|
|
1919
|
-
"
|
|
2103
|
+
"reboot.nodejs.python.task_await(\"" + state_name + "\", \"" + method
|
|
1920
2104
|
+ "\", ...) in nodejs",
|
|
1921
2105
|
js_external_context,
|
|
1922
2106
|
[rbt_module = std::move(rbt_module),
|
|
@@ -1925,7 +2109,7 @@ Napi::Value Task_await(const Napi::CallbackInfo& info) {
|
|
|
1925
2109
|
js_external_context, // Ensures `py_context` remains valid.
|
|
1926
2110
|
py_context,
|
|
1927
2111
|
json_task_id]() {
|
|
1928
|
-
return py::module::import("
|
|
2112
|
+
return py::module::import("reboot.nodejs.python")
|
|
1929
2113
|
.attr("task_await")(
|
|
1930
2114
|
py_context,
|
|
1931
2115
|
py::module::import(rbt_module.c_str()).attr(state_name.c_str()),
|
|
@@ -1973,7 +2157,7 @@ Napi::Value ExternalContext_constructor(const Napi::CallbackInfo& info) {
|
|
|
1973
2157
|
&idempotency_seed,
|
|
1974
2158
|
&idempotency_required,
|
|
1975
2159
|
&idempotency_required_reason]() {
|
|
1976
|
-
py::object py_external = py::module::import("
|
|
2160
|
+
py::object py_external = py::module::import("reboot.aio.external");
|
|
1977
2161
|
|
|
1978
2162
|
auto convert_str =
|
|
1979
2163
|
[](const std::optional<std::string>& optional) -> py::object {
|
|
@@ -2007,6 +2191,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2007
2191
|
NapiSafeFunctionReference(info[0].As<Napi::Function>());
|
|
2008
2192
|
|
|
2009
2193
|
Napi::Array js_servicers = info[1].As<Napi::Array>();
|
|
2194
|
+
auto servicer_details = make_servicer_details(info.Env(), js_servicers);
|
|
2010
2195
|
|
|
2011
2196
|
Napi::Object js_web_framework = info[2].As<Napi::Object>();
|
|
2012
2197
|
|
|
@@ -2016,8 +2201,6 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2016
2201
|
auto js_web_framework_stop = NapiSafeFunctionReference(
|
|
2017
2202
|
js_web_framework.Get("stop").As<Napi::Function>());
|
|
2018
2203
|
|
|
2019
|
-
auto servicer_details = make_servicer_details(info.Env(), js_servicers);
|
|
2020
|
-
|
|
2021
2204
|
auto js_initialize = NapiSafeFunctionReference(info[3].As<Napi::Function>());
|
|
2022
2205
|
|
|
2023
2206
|
std::optional<std::string> initialize_bearer_token;
|
|
@@ -2030,6 +2213,9 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2030
2213
|
js_token_verifier = NapiSafeReference(info[5].As<Napi::Object>());
|
|
2031
2214
|
}
|
|
2032
2215
|
|
|
2216
|
+
Napi::Array js_libraries = info[6].As<Napi::Array>();
|
|
2217
|
+
auto library_details = make_library_details(info.Env(), js_libraries);
|
|
2218
|
+
|
|
2033
2219
|
py::object* py_application = RunCallbackOnPythonEventLoop(
|
|
2034
2220
|
[servicer_details = std::move(servicer_details),
|
|
2035
2221
|
js_web_framework_start = std::move(js_web_framework_start),
|
|
@@ -2037,8 +2223,11 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2037
2223
|
initialize_bearer_token = std::move(initialize_bearer_token),
|
|
2038
2224
|
js_initialize = std::move(js_initialize),
|
|
2039
2225
|
js_token_verifier,
|
|
2040
|
-
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)]() {
|
|
2041
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);
|
|
2042
2231
|
|
|
2043
2232
|
py::object py_web_framework_start = py::cpp_function(
|
|
2044
2233
|
[js_web_framework_start = std::move(js_web_framework_start),
|
|
@@ -2105,7 +2294,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2105
2294
|
|
|
2106
2295
|
return new py::object(
|
|
2107
2296
|
py::module::import(
|
|
2108
|
-
"
|
|
2297
|
+
"reboot.aio.external")
|
|
2109
2298
|
.attr("ExternalContext")(
|
|
2110
2299
|
"name"_a = py::str(name),
|
|
2111
2300
|
"channel_manager"_a =
|
|
@@ -2216,6 +2405,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
|
|
|
2216
2405
|
py::module::import("reboot.aio.applications")
|
|
2217
2406
|
.attr("NodeApplication")(
|
|
2218
2407
|
"servicers"_a = py_servicers,
|
|
2408
|
+
"libraries"_a = py_libraries,
|
|
2219
2409
|
"web_framework_start"_a = py_web_framework_start,
|
|
2220
2410
|
"web_framework_stop"_a = py_web_framework_stop,
|
|
2221
2411
|
"initialize"_a = py_initialize,
|
|
@@ -2245,7 +2435,7 @@ Napi::Value Application_run(const Napi::CallbackInfo& info) {
|
|
|
2245
2435
|
Napi::Promise js_promise = NodePromiseFromPythonTask(
|
|
2246
2436
|
info.Env(),
|
|
2247
2437
|
"Application.run() in nodejs",
|
|
2248
|
-
{"
|
|
2438
|
+
{"reboot.nodejs.python", "create_task"},
|
|
2249
2439
|
[js_external_application, // Ensures `py_application` remains valid.
|
|
2250
2440
|
py_application]() { return py_application->attr("run")(); });
|
|
2251
2441
|
|
|
@@ -2299,10 +2489,10 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
|
|
|
2299
2489
|
if (js_alias.IsString()) {
|
|
2300
2490
|
alias = js_alias.As<Napi::String>().Utf8Value();
|
|
2301
2491
|
}
|
|
2302
|
-
auto
|
|
2303
|
-
std::optional<bool>
|
|
2304
|
-
if (
|
|
2305
|
-
|
|
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>();
|
|
2306
2496
|
}
|
|
2307
2497
|
|
|
2308
2498
|
return NodePromiseFromPythonCallback(
|
|
@@ -2314,11 +2504,11 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
|
|
|
2314
2504
|
method = std::move(method),
|
|
2315
2505
|
key = std::move(key),
|
|
2316
2506
|
alias = std::move(alias),
|
|
2317
|
-
|
|
2507
|
+
per_iteration = std::move(per_iteration)]() {
|
|
2318
2508
|
// Need to use `call_with_context` to ensure that we have
|
|
2319
2509
|
// `py_context` as a valid asyncio context variable.
|
|
2320
2510
|
py::object py_idempotency =
|
|
2321
|
-
py::module::import("
|
|
2511
|
+
py::module::import("reboot.nodejs.python")
|
|
2322
2512
|
.attr("call_with_context")(
|
|
2323
2513
|
py::cpp_function([&]() {
|
|
2324
2514
|
py::object py_key = py::none();
|
|
@@ -2329,16 +2519,17 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
|
|
|
2329
2519
|
if (alias.has_value()) {
|
|
2330
2520
|
py_alias = py::cast(*alias);
|
|
2331
2521
|
}
|
|
2332
|
-
py::object
|
|
2333
|
-
if (
|
|
2334
|
-
|
|
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");
|
|
2335
2526
|
}
|
|
2336
|
-
return py::module::import("
|
|
2527
|
+
return py::module::import("reboot.aio.contexts")
|
|
2337
2528
|
.attr("Context")
|
|
2338
2529
|
.attr("idempotency")(
|
|
2339
2530
|
"key"_a = py_key,
|
|
2340
2531
|
"alias"_a = py_alias,
|
|
2341
|
-
"
|
|
2532
|
+
"how"_a = py_how);
|
|
2342
2533
|
}),
|
|
2343
2534
|
py_context);
|
|
2344
2535
|
|
|
@@ -2394,7 +2585,7 @@ Napi::Value WorkflowContext_loop(const Napi::CallbackInfo& info) {
|
|
|
2394
2585
|
[js_external_context, // Ensures `py_context` remains valid.
|
|
2395
2586
|
py_context,
|
|
2396
2587
|
alias = std::move(alias)]() {
|
|
2397
|
-
return py::module::import("
|
|
2588
|
+
return py::module::import("reboot.nodejs.python")
|
|
2398
2589
|
.attr("loop")(py_context, alias);
|
|
2399
2590
|
},
|
|
2400
2591
|
[](py::object py_iterate) { return new py::object(py_iterate); },
|
|
@@ -2472,7 +2663,7 @@ Napi::Value retry_reactively_until(const Napi::CallbackInfo& info) {
|
|
|
2472
2663
|
});
|
|
2473
2664
|
});
|
|
2474
2665
|
|
|
2475
|
-
return py::module::import("
|
|
2666
|
+
return py::module::import("reboot.aio.contexts")
|
|
2476
2667
|
.attr("retry_reactively_until")(py_context, py_condition);
|
|
2477
2668
|
});
|
|
2478
2669
|
}
|
|
@@ -2524,7 +2715,7 @@ Napi::Value memoize(const Napi::CallbackInfo& info) {
|
|
|
2524
2715
|
});
|
|
2525
2716
|
});
|
|
2526
2717
|
|
|
2527
|
-
return py::module::import("
|
|
2718
|
+
return py::module::import("reboot.aio.memoize")
|
|
2528
2719
|
.attr("memoize")(
|
|
2529
2720
|
py::make_tuple(alias, how),
|
|
2530
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.2";
|
package/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const REBOOT_VERSION = "0.
|
|
1
|
+
export const REBOOT_VERSION = "0.45.2";
|
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
|
|
@@ -401,7 +402,7 @@ const generate = (proto, { schema, path, name, state = false, }) => {
|
|
|
401
402
|
}
|
|
402
403
|
const { tag } = meta;
|
|
403
404
|
if (tags.has(tag)) {
|
|
404
|
-
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`));
|
|
405
406
|
process.exit(-1);
|
|
406
407
|
}
|
|
407
408
|
tags.set(tag, [toSnakeCase(literal), typeName]);
|
|
@@ -424,6 +425,121 @@ const generate = (proto, { schema, path, name, state = false, }) => {
|
|
|
424
425
|
throw new Error(`Unexpected type '${schema._zod.def.type}'`);
|
|
425
426
|
}
|
|
426
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
|
+
};
|
|
427
543
|
const main = async () => {
|
|
428
544
|
// We generate the `.proto` files in a temporary directory.
|
|
429
545
|
const generatedProtosDirectory = await mkdtemp(path.join(os.tmpdir(), "protos-"));
|
|
@@ -501,6 +617,7 @@ const main = async () => {
|
|
|
501
617
|
// It is safe to do so, because right before that we call
|
|
502
618
|
// 'safeParse()', which ensures that the type is correct.
|
|
503
619
|
const type = result.data;
|
|
620
|
+
validateProperDefaultSpecified(type.state instanceof z.ZodObject ? type.state : z.object(type.state), `api.${typeName}.state`);
|
|
504
621
|
generate(proto, {
|
|
505
622
|
schema: type.state instanceof z.ZodObject ? type.state : z.object(type.state),
|
|
506
623
|
path: `api.${typeName}.state`,
|
|
@@ -511,6 +628,7 @@ const main = async () => {
|
|
|
511
628
|
for (const methodName in type.methods) {
|
|
512
629
|
// TODO: ensure `methodName` is PascalCase.
|
|
513
630
|
const { request, response } = type.methods[methodName];
|
|
631
|
+
validateProperDefaultSpecified(request instanceof z.ZodObject ? request : z.object(request), `api.${typeName}.methods.${methodName}.request`);
|
|
514
632
|
const requestTypeName = `${typeName}${toPascalCase(methodName)}Request`;
|
|
515
633
|
generate(proto, {
|
|
516
634
|
schema: request instanceof z.ZodObject ? request : z.object(request),
|
|
@@ -520,6 +638,7 @@ const main = async () => {
|
|
|
520
638
|
if (response instanceof z.ZodVoid) {
|
|
521
639
|
continue;
|
|
522
640
|
}
|
|
641
|
+
validateProperDefaultSpecified(response instanceof z.ZodObject ? response : z.object(response), `api.${typeName}.methods.${methodName}.response`);
|
|
523
642
|
const responseTypeName = `${typeName}${toPascalCase(methodName)}Response`;
|
|
524
643
|
generate(proto, {
|
|
525
644
|
schema: response instanceof z.ZodObject ? response : z.object(response),
|
|
@@ -554,6 +673,7 @@ const main = async () => {
|
|
|
554
673
|
const errors = method.errors instanceof z.ZodDiscriminatedUnion
|
|
555
674
|
? method.errors
|
|
556
675
|
: z.discriminatedUnion("type", method.errors);
|
|
676
|
+
validateProperDefaultSpecified(errors, `api.${typeName}.methods.${methodName}.errors`);
|
|
557
677
|
const path = `api.${typeName}.methods.${methodName}.errors`;
|
|
558
678
|
const name = `${typeName}${toPascalCase(methodName)}Errors`;
|
|
559
679
|
errorsToGenerate.push(() => {
|