@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 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 the `ServiceBuilder`
70
+ First, we create a service using `ServiceSchema`:
71
71
 
72
72
  ```ts
73
- import { ServiceBuilder, Ok, buildServiceDefs } from '@replit/river';
73
+ import { ServicaSchema, Procedure, Ok } from '@replit/river';
74
74
  import { Type } from '@sinclair/typebox';
75
75
 
76
- export const ExampleServiceConstructor = () =>
77
- ServiceBuilder.create('example')
76
+ export const ExampleService = ServiceSchema.define(
77
+ // configuration
78
+ {
78
79
  // initializer for shared state
79
- .initialState({
80
- count: 0,
81
- })
82
- .defineProcedure('add', {
83
- type: 'rpc',
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
- .finalize();
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, serviceDefs);
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/builder.ts
11
+ // router/services.ts
12
12
  import { Type } from "@sinclair/typebox";
13
- function serializeService(s) {
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
- * Finalizes the schema for the service.
15
+ * Factory function for creating a fresh state.
44
16
  */
45
- finalize() {
46
- return Object.freeze(this.schema);
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
- * Sets the initial state for the service.
50
- * @template InitState The type of the initial state.
51
- * @param {InitState} state The initial state for the service.
52
- * @returns {ServiceBuilder<{ name: T['name']; state: InitState; procedures: T['procedures']; }>} A new ServiceBuilder instance with the updated schema.
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
- initialState(state) {
55
- return new _ServiceBuilder({
56
- ...this.schema,
57
- state
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
- * Defines a new procedure for the service.
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
- defineProcedure(procName, procDef) {
67
- const newProcedure = { [procName]: procDef };
68
- const procedures = {
69
- ...this.schema.procedures,
70
- ...newProcedure
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
- * Creates a new instance of ServiceBuilder.
79
- * @param {Name} name The name of the service.
80
- * @returns {ServiceBuilder<{ name: Name; state: {}; procedures: {}; }>} A new instance of ServiceBuilder.
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
- static create(name) {
83
- return new _ServiceBuilder({
84
- name,
85
- state: {},
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/defs.ts
92
- function buildServiceDefs(services) {
93
- return services.reduce((acc, service) => {
94
- acc[service.name] = service;
95
- return acc;
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 Type2
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 = Type2.Object({
391
- code: Type2.Union([
392
- Type2.Literal(UNCAUGHT_ERROR),
393
- Type2.Literal(UNEXPECTED_DISCONNECT)
532
+ var RiverUncaughtSchema = Type3.Object({
533
+ code: Type3.Union([
534
+ Type3.Literal(UNCAUGHT_ERROR),
535
+ Type3.Literal(UNEXPECTED_DISCONNECT)
394
536
  ]),
395
- message: Type2.String()
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
- this.transport = transport;
720
- this.services = services;
861
+ const instances = {};
862
+ this.services = instances;
721
863
  this.contextMap = /* @__PURE__ */ new Map();
722
- this.disconnectedSessions = /* @__PURE__ */ new Set();
723
- for (const service of Object.values(services)) {
724
- this.contextMap.set(service, {
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: service.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 ${service.name}`;
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 stream = this.streamMap.get(id);
1018
- if (!stream) {
1162
+ const stream2 = this.streamMap.get(id);
1163
+ if (!stream2) {
1019
1164
  return;
1020
1165
  }
1021
- stream.incoming.end();
1022
- await stream.promises.inputHandler;
1023
- stream.outgoing.end();
1024
- await stream.promises.outputHandler;
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
- serializeService,
1034
- ServiceBuilder,
1035
- buildServiceDefs,
1178
+ ServiceSchema,
1179
+ Procedure,
1036
1180
  pushable,
1037
1181
  UNCAUGHT_ERROR,
1038
1182
  RiverUncaughtSchema,