@replit/river 0.15.7 → 0.16.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 +17 -15
- package/dist/{chunk-KWXQLQAF.js → chunk-Q6WPGM3K.js} +237 -93
- package/dist/procedures-f0226890.d.ts +305 -0
- package/dist/router/index.cjs +271 -128
- package/dist/router/index.d.cts +304 -20
- package/dist/router/index.d.ts +304 -20
- package/dist/router/index.js +7 -9
- package/dist/util/testHelpers.d.cts +4 -4
- package/dist/util/testHelpers.d.ts +4 -4
- package/dist/util/testHelpers.js +1 -1
- package/package.json +1 -1
- package/dist/builder-4d392f6c.d.ts +0 -241
package/README.md
CHANGED
|
@@ -67,20 +67,21 @@ npm i ws isomorphic-ws
|
|
|
67
67
|
|
|
68
68
|
### A basic router
|
|
69
69
|
|
|
70
|
-
First, we create a service using
|
|
70
|
+
First, we create a service using `ServiceSchema`:
|
|
71
71
|
|
|
72
72
|
```ts
|
|
73
|
-
import {
|
|
73
|
+
import { ServicaSchema, Procedure, Ok } from '@replit/river';
|
|
74
74
|
import { Type } from '@sinclair/typebox';
|
|
75
75
|
|
|
76
|
-
export const
|
|
77
|
-
|
|
76
|
+
export const ExampleService = ServiceSchema.define(
|
|
77
|
+
// configuration
|
|
78
|
+
{
|
|
78
79
|
// initializer for shared state
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
initializeState: () => ({ count: 0 }),
|
|
81
|
+
},
|
|
82
|
+
// procedures
|
|
83
|
+
{
|
|
84
|
+
add: Procedure.rpc({
|
|
84
85
|
input: Type.Object({ n: Type.Number() }),
|
|
85
86
|
output: Type.Object({ result: Type.Number() }),
|
|
86
87
|
errors: Type.Never(),
|
|
@@ -90,11 +91,9 @@ export const ExampleServiceConstructor = () =>
|
|
|
90
91
|
ctx.state.count += n;
|
|
91
92
|
return Ok({ result: ctx.state.count });
|
|
92
93
|
},
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// expore a listing of all the services that we have
|
|
97
|
-
export const serviceDefs = buildServiceDefs([ExampleServiceConstructor()]);
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
);
|
|
98
97
|
```
|
|
99
98
|
|
|
100
99
|
Then, we create the server:
|
|
@@ -111,7 +110,10 @@ const port = 3000;
|
|
|
111
110
|
const wss = new WebSocketServer({ server: httpServer });
|
|
112
111
|
const transport = new WebSocketServerTransport(wss, 'SERVER');
|
|
113
112
|
|
|
114
|
-
export const server = createServer(transport,
|
|
113
|
+
export const server = createServer(transport, {
|
|
114
|
+
example: ExampleService,
|
|
115
|
+
});
|
|
116
|
+
|
|
115
117
|
export type ServiceSurface = typeof server;
|
|
116
118
|
|
|
117
119
|
httpServer.listen(port);
|
|
@@ -8,93 +8,235 @@ import {
|
|
|
8
8
|
log
|
|
9
9
|
} from "./chunk-H4BYJELI.js";
|
|
10
10
|
|
|
11
|
-
// router/
|
|
11
|
+
// router/services.ts
|
|
12
12
|
import { Type } from "@sinclair/typebox";
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
name: s.name,
|
|
16
|
-
state: s.state,
|
|
17
|
-
procedures: Object.fromEntries(
|
|
18
|
-
Object.entries(s.procedures).map(([procName, procDef]) => [
|
|
19
|
-
procName,
|
|
20
|
-
{
|
|
21
|
-
input: Type.Strict(procDef.input),
|
|
22
|
-
output: Type.Strict(procDef.output),
|
|
23
|
-
// Only add the `errors` field if it is non-never.
|
|
24
|
-
..."errors" in procDef ? {
|
|
25
|
-
errors: Type.Strict(procDef.errors)
|
|
26
|
-
} : {},
|
|
27
|
-
type: procDef.type,
|
|
28
|
-
// Only add the `init` field if the type declares it.
|
|
29
|
-
..."init" in procDef ? {
|
|
30
|
-
init: Type.Strict(procDef.init)
|
|
31
|
-
} : {}
|
|
32
|
-
}
|
|
33
|
-
])
|
|
34
|
-
)
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
var ServiceBuilder = class _ServiceBuilder {
|
|
38
|
-
schema;
|
|
39
|
-
constructor(schema) {
|
|
40
|
-
this.schema = schema;
|
|
41
|
-
}
|
|
13
|
+
var ServiceSchema = class _ServiceSchema {
|
|
42
14
|
/**
|
|
43
|
-
*
|
|
15
|
+
* Factory function for creating a fresh state.
|
|
44
16
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
17
|
+
initializeState;
|
|
18
|
+
/**
|
|
19
|
+
* The procedures for this service.
|
|
20
|
+
*/
|
|
21
|
+
procedures;
|
|
22
|
+
/**
|
|
23
|
+
* @param config - The configuration for this service.
|
|
24
|
+
* @param procedures - The procedures for this service.
|
|
25
|
+
*/
|
|
26
|
+
constructor(config, procedures) {
|
|
27
|
+
this.initializeState = config.initializeState;
|
|
28
|
+
this.procedures = procedures;
|
|
47
29
|
}
|
|
48
30
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
31
|
+
* Creates a {@link ServiceScaffold}, which can be used to define procedures
|
|
32
|
+
* that can then be merged into a {@link ServiceSchema}, via the scaffold's
|
|
33
|
+
* `finalize` method.
|
|
34
|
+
*
|
|
35
|
+
* There are two patterns that work well with this method. The first is using
|
|
36
|
+
* it to separate the definition of procedures from the definition of the
|
|
37
|
+
* service's configuration:
|
|
38
|
+
* ```ts
|
|
39
|
+
* const MyServiceScaffold = ServiceSchema.scaffold({
|
|
40
|
+
* initializeState: () => ({ count: 0 }),
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* const incrementProcedures = MyServiceScaffold.procedures({
|
|
44
|
+
* increment: Procedure.rpc({
|
|
45
|
+
* input: Type.Object({ amount: Type.Number() }),
|
|
46
|
+
* output: Type.Object({ current: Type.Number() }),
|
|
47
|
+
* async handler(ctx, input) {
|
|
48
|
+
* ctx.state.count += input.amount;
|
|
49
|
+
* return Ok({ current: ctx.state.count });
|
|
50
|
+
* }
|
|
51
|
+
* }),
|
|
52
|
+
* })
|
|
53
|
+
*
|
|
54
|
+
* const MyService = MyServiceScaffold.finalize({
|
|
55
|
+
* ...incrementProcedures,
|
|
56
|
+
* // you can also directly define procedures here
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
* This might be really handy if you have a very large service and you're
|
|
60
|
+
* wanting to split it over multiple files. You can define the scaffold
|
|
61
|
+
* in one file, and then import that scaffold in other files where you
|
|
62
|
+
* define procedures - and then finally import the scaffolds and your
|
|
63
|
+
* procedure objects in a final file where you finalize the scaffold into
|
|
64
|
+
* a service schema.
|
|
65
|
+
*
|
|
66
|
+
* The other way is to use it like in a builder pattern:
|
|
67
|
+
* ```ts
|
|
68
|
+
* const MyService = ServiceSchema
|
|
69
|
+
* .scaffold({ initializeState: () => ({ count: 0 }) })
|
|
70
|
+
* .finalize({
|
|
71
|
+
* increment: Procedure.rpc({
|
|
72
|
+
* input: Type.Object({ amount: Type.Number() }),
|
|
73
|
+
* output: Type.Object({ current: Type.Number() }),
|
|
74
|
+
* async handler(ctx, input) {
|
|
75
|
+
* ctx.state.count += input.amount;
|
|
76
|
+
* return Ok({ current: ctx.state.count });
|
|
77
|
+
* }
|
|
78
|
+
* }),
|
|
79
|
+
* })
|
|
80
|
+
* ```
|
|
81
|
+
* Depending on your preferences, this may be a more appealing way to define
|
|
82
|
+
* a schema versus using the {@link ServiceSchema.define} method.
|
|
53
83
|
*/
|
|
54
|
-
|
|
55
|
-
return new
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
84
|
+
static scaffold(config) {
|
|
85
|
+
return new ServiceScaffold(config);
|
|
86
|
+
}
|
|
87
|
+
// actual implementation
|
|
88
|
+
static define(configOrProcedures, maybeProcedures) {
|
|
89
|
+
let config;
|
|
90
|
+
let procedures;
|
|
91
|
+
if ("initializeState" in configOrProcedures && typeof configOrProcedures.initializeState === "function") {
|
|
92
|
+
if (!maybeProcedures) {
|
|
93
|
+
throw new Error("Expected procedures to be defined");
|
|
94
|
+
}
|
|
95
|
+
config = configOrProcedures;
|
|
96
|
+
procedures = maybeProcedures;
|
|
97
|
+
} else {
|
|
98
|
+
config = { initializeState: () => ({}) };
|
|
99
|
+
procedures = configOrProcedures;
|
|
100
|
+
}
|
|
101
|
+
return new _ServiceSchema(config, procedures);
|
|
59
102
|
}
|
|
60
103
|
/**
|
|
61
|
-
*
|
|
62
|
-
* @param {ProcName} procName The name of the procedure.
|
|
63
|
-
* @param {Procedure<T['state'], Ty, I, O, E, Init>} procDef The definition of the procedure.
|
|
64
|
-
* @returns {ServiceBuilder<{ name: T['name']; state: T['state']; procedures: T['procedures'] & { [k in ProcName]: Procedure<T['state'], Ty, I, O, E, Init>; }; }>} A new ServiceBuilder instance with the updated schema.
|
|
104
|
+
* Serializes this schema's procedures into a plain object that is JSON compatible.
|
|
65
105
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
serialize() {
|
|
107
|
+
return {
|
|
108
|
+
procedures: Object.fromEntries(
|
|
109
|
+
Object.entries(this.procedures).map(([procName, procDef]) => [
|
|
110
|
+
procName,
|
|
111
|
+
{
|
|
112
|
+
input: Type.Strict(procDef.input),
|
|
113
|
+
output: Type.Strict(procDef.output),
|
|
114
|
+
// Only add the `errors` field if it is non-never.
|
|
115
|
+
..."errors" in procDef ? {
|
|
116
|
+
errors: Type.Strict(procDef.errors)
|
|
117
|
+
} : {},
|
|
118
|
+
type: procDef.type,
|
|
119
|
+
// Only add the `init` field if the type declares it.
|
|
120
|
+
..."init" in procDef ? {
|
|
121
|
+
init: Type.Strict(procDef.init)
|
|
122
|
+
} : {}
|
|
123
|
+
}
|
|
124
|
+
])
|
|
125
|
+
)
|
|
71
126
|
};
|
|
72
|
-
return new _ServiceBuilder({
|
|
73
|
-
...this.schema,
|
|
74
|
-
procedures
|
|
75
|
-
});
|
|
76
127
|
}
|
|
77
128
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
129
|
+
* Instantiates this schema into a {@link Service} object.
|
|
130
|
+
*
|
|
131
|
+
* You probably don't need this, usually the River server will handle this
|
|
132
|
+
* for you.
|
|
81
133
|
*/
|
|
82
|
-
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
procedures: {}
|
|
134
|
+
instantiate() {
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
state: this.initializeState(),
|
|
137
|
+
procedures: this.procedures
|
|
87
138
|
});
|
|
88
139
|
}
|
|
89
140
|
};
|
|
141
|
+
var ServiceScaffold = class {
|
|
142
|
+
/**
|
|
143
|
+
* The configuration for this service.
|
|
144
|
+
*/
|
|
145
|
+
config;
|
|
146
|
+
/**
|
|
147
|
+
* @param config - The configuration for this service.
|
|
148
|
+
*/
|
|
149
|
+
constructor(config) {
|
|
150
|
+
this.config = config;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Define procedures for this service. Use the {@link Procedure} constructors
|
|
154
|
+
* to create them. This returns the procedures object, which can then be
|
|
155
|
+
* passed to {@link ServiceSchema.finalize} to create a {@link ServiceSchema}.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```
|
|
159
|
+
* const myProcedures = MyServiceScaffold.procedures({
|
|
160
|
+
* myRPC: Procedure.rpc({
|
|
161
|
+
* // ...
|
|
162
|
+
* }),
|
|
163
|
+
* });
|
|
164
|
+
*
|
|
165
|
+
* const MyService = MyServiceScaffold.finalize({
|
|
166
|
+
* ...myProcedures,
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @param procedures - The procedures for this service.
|
|
171
|
+
*/
|
|
172
|
+
procedures(procedures) {
|
|
173
|
+
return procedures;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Finalizes the scaffold into a {@link ServiceSchema}. This is where you
|
|
177
|
+
* provide the service's procedures and get a {@link ServiceSchema} in return.
|
|
178
|
+
*
|
|
179
|
+
* You can directly define procedures here, or you can define them separately
|
|
180
|
+
* with the {@link ServiceScaffold.procedures} method, and then pass them here.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```
|
|
184
|
+
* const MyService = MyServiceScaffold.finalize({
|
|
185
|
+
* myRPC: Procedure.rpc({
|
|
186
|
+
* // ...
|
|
187
|
+
* }),
|
|
188
|
+
* // e.g. from the procedures method
|
|
189
|
+
* ...myOtherProcedures,
|
|
190
|
+
* });
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
finalize(procedures) {
|
|
194
|
+
return ServiceSchema.define(this.config, procedures);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
90
197
|
|
|
91
|
-
// router/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
198
|
+
// router/procedures.ts
|
|
199
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
200
|
+
function rpc({
|
|
201
|
+
input,
|
|
202
|
+
output,
|
|
203
|
+
errors = Type2.Never(),
|
|
204
|
+
handler
|
|
205
|
+
}) {
|
|
206
|
+
return { type: "rpc", input, output, errors, handler };
|
|
207
|
+
}
|
|
208
|
+
function upload({
|
|
209
|
+
init,
|
|
210
|
+
input,
|
|
211
|
+
output,
|
|
212
|
+
errors = Type2.Never(),
|
|
213
|
+
handler
|
|
214
|
+
}) {
|
|
215
|
+
return init !== void 0 && init !== null ? { type: "upload", init, input, output, errors, handler } : { type: "upload", input, output, errors, handler };
|
|
216
|
+
}
|
|
217
|
+
function subscription({
|
|
218
|
+
input,
|
|
219
|
+
output,
|
|
220
|
+
errors = Type2.Never(),
|
|
221
|
+
handler
|
|
222
|
+
}) {
|
|
223
|
+
return { type: "subscription", input, output, errors, handler };
|
|
97
224
|
}
|
|
225
|
+
function stream({
|
|
226
|
+
init,
|
|
227
|
+
input,
|
|
228
|
+
output,
|
|
229
|
+
errors = Type2.Never(),
|
|
230
|
+
handler
|
|
231
|
+
}) {
|
|
232
|
+
return init !== void 0 && init !== null ? { type: "stream", init, input, output, errors, handler } : { type: "stream", input, output, errors, handler };
|
|
233
|
+
}
|
|
234
|
+
var Procedure = {
|
|
235
|
+
rpc,
|
|
236
|
+
upload,
|
|
237
|
+
subscription,
|
|
238
|
+
stream
|
|
239
|
+
};
|
|
98
240
|
|
|
99
241
|
// node_modules/p-defer/index.js
|
|
100
242
|
function pDefer() {
|
|
@@ -383,16 +525,16 @@ import { nanoid } from "nanoid";
|
|
|
383
525
|
|
|
384
526
|
// router/result.ts
|
|
385
527
|
import {
|
|
386
|
-
Type as
|
|
528
|
+
Type as Type3
|
|
387
529
|
} from "@sinclair/typebox";
|
|
388
530
|
var UNCAUGHT_ERROR = "UNCAUGHT_ERROR";
|
|
389
531
|
var UNEXPECTED_DISCONNECT = "UNEXPECTED_DISCONNECT";
|
|
390
|
-
var RiverUncaughtSchema =
|
|
391
|
-
code:
|
|
392
|
-
|
|
393
|
-
|
|
532
|
+
var RiverUncaughtSchema = Type3.Object({
|
|
533
|
+
code: Type3.Union([
|
|
534
|
+
Type3.Literal(UNCAUGHT_ERROR),
|
|
535
|
+
Type3.Literal(UNEXPECTED_DISCONNECT)
|
|
394
536
|
]),
|
|
395
|
-
message:
|
|
537
|
+
message: Type3.String()
|
|
396
538
|
});
|
|
397
539
|
function Ok(payload) {
|
|
398
540
|
return {
|
|
@@ -716,16 +858,19 @@ var RiverServer = class {
|
|
|
716
858
|
clientStreams;
|
|
717
859
|
disconnectedSessions;
|
|
718
860
|
constructor(transport, services, extendedContext) {
|
|
719
|
-
|
|
720
|
-
this.services =
|
|
861
|
+
const instances = {};
|
|
862
|
+
this.services = instances;
|
|
721
863
|
this.contextMap = /* @__PURE__ */ new Map();
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
864
|
+
for (const [name, service] of Object.entries(services)) {
|
|
865
|
+
const instance = service.instantiate();
|
|
866
|
+
instances[name] = instance;
|
|
867
|
+
this.contextMap.set(instance, {
|
|
725
868
|
...extendedContext,
|
|
726
|
-
state:
|
|
869
|
+
state: instance.state
|
|
727
870
|
});
|
|
728
871
|
}
|
|
872
|
+
this.transport = transport;
|
|
873
|
+
this.disconnectedSessions = /* @__PURE__ */ new Set();
|
|
729
874
|
this.streamMap = /* @__PURE__ */ new Map();
|
|
730
875
|
this.clientStreams = /* @__PURE__ */ new Map();
|
|
731
876
|
this.transport.addEventListener("message", this.onMessage);
|
|
@@ -794,7 +939,7 @@ var RiverServer = class {
|
|
|
794
939
|
return;
|
|
795
940
|
}
|
|
796
941
|
const service = this.services[message.serviceName];
|
|
797
|
-
const serviceContext = this.getContext(service);
|
|
942
|
+
const serviceContext = this.getContext(service, message.serviceName);
|
|
798
943
|
if (!(message.procedureName in service.procedures)) {
|
|
799
944
|
log?.warn(
|
|
800
945
|
`${this.transport.clientId} -- couldn't find a matching procedure for ${message.serviceName}.${message.procedureName}`
|
|
@@ -1004,24 +1149,24 @@ var RiverServer = class {
|
|
|
1004
1149
|
}
|
|
1005
1150
|
}
|
|
1006
1151
|
}
|
|
1007
|
-
getContext(service) {
|
|
1152
|
+
getContext(service, name) {
|
|
1008
1153
|
const context = this.contextMap.get(service);
|
|
1009
1154
|
if (!context) {
|
|
1010
|
-
const err = `${this.transport.clientId} -- no context found for ${
|
|
1155
|
+
const err = `${this.transport.clientId} -- no context found for ${name}`;
|
|
1011
1156
|
log?.error(err);
|
|
1012
1157
|
throw new Error(err);
|
|
1013
1158
|
}
|
|
1014
1159
|
return context;
|
|
1015
1160
|
}
|
|
1016
1161
|
cleanupStream = async (id) => {
|
|
1017
|
-
const
|
|
1018
|
-
if (!
|
|
1162
|
+
const stream2 = this.streamMap.get(id);
|
|
1163
|
+
if (!stream2) {
|
|
1019
1164
|
return;
|
|
1020
1165
|
}
|
|
1021
|
-
|
|
1022
|
-
await
|
|
1023
|
-
|
|
1024
|
-
await
|
|
1166
|
+
stream2.incoming.end();
|
|
1167
|
+
await stream2.promises.inputHandler;
|
|
1168
|
+
stream2.outgoing.end();
|
|
1169
|
+
await stream2.promises.outputHandler;
|
|
1025
1170
|
this.streamMap.delete(id);
|
|
1026
1171
|
};
|
|
1027
1172
|
};
|
|
@@ -1030,9 +1175,8 @@ function createServer(transport, services, extendedContext) {
|
|
|
1030
1175
|
}
|
|
1031
1176
|
|
|
1032
1177
|
export {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
buildServiceDefs,
|
|
1178
|
+
ServiceSchema,
|
|
1179
|
+
Procedure,
|
|
1036
1180
|
pushable,
|
|
1037
1181
|
UNCAUGHT_ERROR,
|
|
1038
1182
|
RiverUncaughtSchema,
|