@jskit-ai/realtime 0.1.155 → 0.1.157

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.
@@ -1,782 +1,239 @@
1
1
  import assert from "node:assert/strict";
2
- import test from "node:test";
3
2
  import { createServer } from "node:http";
4
- import { installServiceRegistrationApi } from "@jskit-ai/kernel/server/runtime";
3
+ import test from "node:test";
4
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
5
+ import { EventProvider } from "@jskit-ai/kernel/server/runtime";
5
6
  import { CLIENT_APP_CONFIG_GLOBAL_KEY, setClientAppConfig } from "../../kernel/client/appConfig.js";
6
-
7
- import { RealtimeServiceProvider } from "../src/server/RealtimeServiceProvider.js";
8
7
  import { RealtimeClientProvider } from "../src/client/RealtimeClientProvider.js";
9
- import { registerRealtimeClientListener } from "../src/client/listeners.js";
10
-
11
- function normalizeDomainEventListener(entry) {
12
- if (typeof entry === "function") {
13
- return {
14
- listenerId: String(entry.name || "anonymous"),
15
- matches: null,
16
- handle: entry
17
- };
18
- }
19
- if (entry && typeof entry === "object" && typeof entry.handle === "function") {
20
- return {
21
- ...entry,
22
- listenerId: String(entry.listenerId || "anonymous"),
23
- matches: typeof entry.matches === "function" ? entry.matches : null
24
- };
25
- }
26
- return null;
27
- }
8
+ import { RealtimeProvider } from "../src/server/RealtimeProvider.js";
9
+ import { registerSocketAudienceBootstrap } from "../src/server/realtimeAudience.js";
10
+ import { createRealtimeDelivery } from "../src/server/realtimeDelivery.js";
28
11
 
29
- function createDomainEvents(scope) {
30
- return Object.freeze({
31
- async publish(event = {}) {
32
- const payload = event && typeof event === "object" && !Array.isArray(event) ? event : {};
33
- const listeners =
34
- typeof scope?.resolveTag === "function" ? scope.resolveTag("jskit.runtime.domainEvent.listeners") : [];
35
- for (const listenerEntry of listeners) {
36
- const listener = normalizeDomainEventListener(listenerEntry);
37
- if (!listener) {
38
- continue;
39
- }
40
- if (listener.matches && listener.matches(payload) !== true) {
41
- continue;
42
- }
43
- await listener.handle(payload);
44
- }
45
- return null;
46
- }
47
- });
48
- }
12
+ const logger = Object.freeze({ debug() {}, info() {}, warn() {}, error() {} });
49
13
 
50
- function createSingletonApp() {
51
- const instances = new Map();
52
- const singletons = new Map();
53
- const tags = new Map();
14
+ function createIoDouble() {
15
+ const emitted = [];
54
16
  return {
55
- instances,
56
- singletons,
57
- tags,
58
- singleton(token, factory) {
59
- singletons.set(token, factory);
60
- },
61
- instance(token, value) {
62
- instances.set(token, value);
63
- },
64
- has(token) {
65
- return instances.has(token) || singletons.has(token);
66
- },
67
- tag(token, tagName) {
68
- const normalizedTagName = String(tagName || "").trim();
69
- if (!tags.has(normalizedTagName)) {
70
- tags.set(normalizedTagName, new Set());
71
- }
72
- tags.get(normalizedTagName).add(token);
73
- },
74
- resolveTag(tagName) {
75
- const normalizedTagName = String(tagName || "").trim();
76
- const tagged = tags.get(normalizedTagName);
77
- if (!tagged || tagged.size < 1) {
78
- return [];
79
- }
80
- return [...tagged].map((token) => this.make(token));
81
- },
82
- make(token) {
83
- if (instances.has(token)) {
84
- return instances.get(token);
85
- }
86
- if (!singletons.has(token)) {
87
- throw new Error(`Missing token: ${String(token)}`);
88
- }
89
- const resolved = singletons.get(token)(this);
90
- instances.set(token, resolved);
91
- return resolved;
17
+ emitted,
18
+ emit(eventName, payload) { emitted.push({ room: null, eventName, payload }); },
19
+ to(room) {
20
+ return {
21
+ emit(eventName, payload) { emitted.push({ room, eventName, payload }); }
22
+ };
92
23
  }
93
24
  };
94
25
  }
95
26
 
96
- test("RealtimeServiceProvider registers runtime realtime server api", () => {
97
- const app = createSingletonApp();
98
- app.instance("jskit.fastify", {
99
- server: createServer()
27
+ test("RealtimeProvider assembles an event-driven runtime capability", async () => {
28
+ let events = null;
29
+ let realtime = null;
30
+ const probe = defineProvider({
31
+ id: "test.realtime.probe",
32
+ requires: { eventsCapability: "runtime.events", realtimeCapability: "runtime.realtime" },
33
+ setup({ eventsCapability, realtimeCapability }) {
34
+ events = eventsCapability;
35
+ realtime = realtimeCapability;
36
+ return {};
37
+ }
100
38
  });
101
- const provider = new RealtimeServiceProvider();
102
- provider.register(app);
103
-
104
- assert.equal(app.singletons.has("runtime.realtime"), true);
105
- assert.equal(app.singletons.has("runtime.realtime.io"), true);
106
-
107
- const api = app.make("runtime.realtime");
108
- assert.equal(typeof api.createSocketIoServer, "function");
109
- assert.equal(typeof api.closeSocketIoServer, "function");
110
- });
111
-
112
- test("RealtimeServiceProvider boot starts socket io and shutdown closes it", async () => {
113
- const app = createSingletonApp();
114
- app.instance("jskit.fastify", {
115
- server: createServer()
39
+ const fastify = { server: createServer() };
40
+ const runtime = createCapabilityRuntime({
41
+ inputs: {
42
+ "runtime.config": {},
43
+ "runtime.env": {},
44
+ "runtime.fastify": fastify,
45
+ "runtime.logger": logger
46
+ },
47
+ providers: [EventProvider, RealtimeProvider, probe]
116
48
  });
117
49
 
118
- const provider = new RealtimeServiceProvider();
119
- provider.register(app);
120
- provider.boot(app);
121
-
122
- const io = app.make("runtime.realtime.io");
123
- assert.equal(Boolean(io), true);
124
- assert.equal(typeof io.on, "function");
125
-
126
- await provider.shutdown(app);
50
+ await runtime.start();
51
+ assert.deepEqual(events.diagnostics().listenerIds, ["runtime.realtime.delivery"]);
52
+ assert.equal(typeof realtime.diagnostics, "function");
53
+ assert.equal(realtime.diagnostics().redisConfigured, false);
54
+ await runtime.shutdown();
127
55
  });
128
56
 
129
- test("RealtimeServiceProvider boot does not eagerly resolve optional auth/workspace bindings", async () => {
130
- const app = createSingletonApp();
131
- app.instance("jskit.fastify", {
132
- server: createServer()
133
- });
134
- app.singleton("authService", () => {
135
- throw new Error("authService should not resolve during realtime boot");
136
- });
137
- app.singleton("internal.repository.workspace-memberships", () => {
138
- throw new Error("workspace memberships repository should not resolve during realtime boot");
57
+ test("realtime delivery sends explicit action events to their selected rooms", async () => {
58
+ const io = createIoDouble();
59
+ const delivery = createRealtimeDelivery({ io, logger });
60
+ await delivery.handle({
61
+ type: "entity.changed",
62
+ source: "workspace",
63
+ entity: "settings",
64
+ operation: "updated",
65
+ entityId: "11",
66
+ scope: { kind: "workspace", id: "11" },
67
+ realtime: {
68
+ event: "workspace.settings.changed",
69
+ audience: "event_scope",
70
+ payload: { workspaceSlug: "acme" }
71
+ }
139
72
  });
140
73
 
141
- const provider = new RealtimeServiceProvider();
142
- provider.register(app);
143
-
144
- await assert.doesNotReject(() => provider.boot(app));
145
- await provider.shutdown(app);
74
+ assert.deepEqual(io.emitted, [{
75
+ room: "workspace:11",
76
+ eventName: "workspace.settings.changed",
77
+ payload: {
78
+ workspaceSlug: "acme",
79
+ type: "entity.changed",
80
+ source: "workspace",
81
+ entity: "settings",
82
+ operation: "updated",
83
+ entityId: "11",
84
+ scope: { kind: "workspace", id: "11" }
85
+ }
86
+ }]);
87
+ assert.equal(Object.hasOwn(io.emitted[0].payload, "realtime"), false);
146
88
  });
147
89
 
148
- test("RealtimeServiceProvider boot authenticates sockets from handshake cookies and joins actor workspace rooms", async () => {
149
- const app = createSingletonApp();
150
- app.instance("jskit.fastify", {
151
- server: createServer()
90
+ test("realtime delivery resolves an explicit database-backed audience without exposing query controls", async () => {
91
+ const io = createIoDouble();
92
+ const delivery = createRealtimeDelivery({
93
+ io,
94
+ logger,
95
+ database: {
96
+ knex() {}
97
+ }
152
98
  });
153
-
154
- const authenticateCalls = [];
155
- app.singleton("authService", () => ({
156
- async authenticateRequest(input = {}) {
157
- authenticateCalls.push(input);
158
- return {
159
- authenticated: true,
160
- actor: {
161
- id: 9
99
+ await delivery.handle({
100
+ type: "entity.changed",
101
+ source: "workspace",
102
+ entity: "invite",
103
+ operation: "created",
104
+ entityId: "91",
105
+ realtime: {
106
+ event: "users.bootstrap.changed",
107
+ audience: {
108
+ preset: "none",
109
+ async userQuery({ knex, event }) {
110
+ assert.equal(typeof knex, "function");
111
+ assert.equal(event.entityId, "91");
112
+ return [{ user_id: 55 }];
162
113
  }
163
- };
164
- }
165
- }));
166
- app.singleton("internal.repository.workspace-memberships", () => ({
167
- async listActiveWorkspaceIdsByUserId(userId) {
168
- assert.equal(userId, "9");
169
- return [11, 12];
114
+ }
170
115
  }
171
- }));
116
+ });
117
+ assert.equal(io.emitted.length, 1);
118
+ assert.equal(io.emitted[0].room, "user:55");
119
+ assert.equal(Object.hasOwn(io.emitted[0].payload, "realtime"), false);
120
+ });
172
121
 
122
+ test("socket audience bootstrap authenticates explicitly and joins actor workspace rooms", async () => {
173
123
  let connectionHandler = null;
174
124
  const io = {
175
125
  on(eventName, handler) {
176
- if (eventName === "connection") {
177
- connectionHandler = handler;
178
- }
126
+ if (eventName === "connection") connectionHandler = handler;
179
127
  }
180
128
  };
181
-
182
- const provider = new RealtimeServiceProvider();
183
- provider.register(app);
184
- app.instance("runtime.realtime.io", io);
185
-
186
- await provider.boot(app);
187
-
188
- const joinedRooms = [];
189
- const socket = {
190
- handshake: {
191
- headers: {
192
- cookie: "session=abc123; theme=dark",
193
- host: "127.0.0.1:3100"
194
- },
195
- address: "127.0.0.1"
196
- },
197
- request: {
198
- headers: {},
199
- socket: {
200
- remoteAddress: "127.0.0.1"
129
+ const authenticateCalls = [];
130
+ registerSocketAudienceBootstrap({
131
+ io,
132
+ logger,
133
+ authService: {
134
+ async authenticateRequest(input) {
135
+ authenticateCalls.push(input);
136
+ return { authenticated: true, actor: { id: 9 } };
201
137
  }
202
138
  },
203
- data: {},
204
- join(room) {
205
- joinedRooms.push(room);
139
+ workspaces: {
140
+ repositories: {
141
+ workspaceMemberships: {
142
+ async listActiveWorkspaceIdsByUserId(userId) {
143
+ assert.equal(userId, "9");
144
+ return [11, 12];
145
+ }
146
+ }
147
+ }
206
148
  }
149
+ });
150
+ const joinedRooms = [];
151
+ const socket = {
152
+ handshake: { headers: { cookie: "session=abc123; theme=dark", host: "127.0.0.1:3100" } },
153
+ request: { headers: {}, socket: { remoteAddress: "127.0.0.1" } },
154
+ data: {},
155
+ join(room) { joinedRooms.push(room); }
207
156
  };
208
-
209
157
  await connectionHandler(socket);
210
- await provider.shutdown(app);
211
-
212
- assert.deepEqual(authenticateCalls, [
213
- {
214
- cookies: {
215
- session: "abc123",
216
- theme: "dark"
217
- },
218
- headers: {
219
- host: "127.0.0.1:3100"
220
- },
221
- socket: {
222
- remoteAddress: "127.0.0.1"
223
- }
224
- }
225
- ]);
158
+ assert.deepEqual(authenticateCalls, [{
159
+ cookies: { session: "abc123", theme: "dark" },
160
+ headers: { host: "127.0.0.1:3100" },
161
+ socket: { remoteAddress: "127.0.0.1" }
162
+ }]);
226
163
  assert.equal(socket.data.actorId, "9");
227
164
  assert.deepEqual(joinedRooms, [
228
- "clients",
229
- "users",
230
- "user:9",
231
- "workspace:11",
232
- "workspace:11:user:9",
233
- "workspace:12",
234
- "workspace:12:user:9"
165
+ "clients", "users", "user:9",
166
+ "workspace:11", "workspace:11:user:9",
167
+ "workspace:12", "workspace:12:user:9"
235
168
  ]);
236
169
  });
237
170
 
238
- test("RealtimeClientProvider registers runtime realtime client api", () => {
239
- const app = createSingletonApp();
240
- const provider = new RealtimeClientProvider();
241
- provider.register(app);
242
-
243
- assert.equal(app.singletons.has("runtime.realtime.client"), true);
244
- assert.equal(app.singletons.has("runtime.realtime.client.socket"), true);
245
- assert.equal(app.singletons.has("realtime.web.connection.indicator"), true);
246
- const api = app.make("runtime.realtime.client");
247
- assert.equal(typeof api.createSocketIoClient, "function");
248
- assert.equal(typeof api.disconnectSocketIoClient, "function");
249
- });
250
-
251
- test("RealtimeClientProvider uses mobile.apiBaseUrl only inside the Capacitor runtime", () => {
252
- const previousAppConfig = globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
253
- const calls = [];
254
- const socket = {
255
- on() {},
256
- off() {},
257
- disconnect() {}
258
- };
259
- try {
260
- setClientAppConfig({
261
- mobile: {
262
- enabled: true,
263
- apiBaseUrl: "http://127.0.0.1:3000"
264
- }
265
- });
266
-
267
- const app = createSingletonApp();
268
- app.instance("mobile.capacitor.adapter.client", {
269
- available: true
270
- });
271
- app.instance("runtime.realtime.client", {
272
- createSocketIoClient(input = {}) {
273
- calls.push(input);
274
- return socket;
275
- },
276
- disconnectSocketIoClient() {}
277
- });
278
-
279
- const provider = new RealtimeClientProvider();
280
- provider.register(app);
281
-
282
- assert.equal(app.make("runtime.realtime.client.socket"), socket);
283
- assert.deepEqual(calls, [
284
- {
285
- url: "http://127.0.0.1:3000",
286
- options: {}
287
- }
288
- ]);
289
- } finally {
290
- if (previousAppConfig === undefined) {
291
- delete globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
292
- } else {
293
- globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY] = previousAppConfig;
171
+ async function startRealtimeClient({ mobile = null } = {}) {
172
+ const registrations = new Map();
173
+ const provided = new Map();
174
+ let realtime = null;
175
+ const probe = defineProvider({
176
+ id: "test.realtime.client.probe",
177
+ requires: { value: "client.realtime" },
178
+ setup({ value }) {
179
+ realtime = value;
180
+ return {};
294
181
  }
295
- }
296
- });
297
-
298
- test("RealtimeClientProvider keeps web socket connections URL-less when mobile is installed outside Capacitor", () => {
299
- const previousAppConfig = globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
300
- const calls = [];
301
- const socket = {
302
- on() {},
303
- off() {},
304
- disconnect() {}
305
- };
306
- try {
307
- setClientAppConfig({
308
- mobile: {
309
- enabled: true,
310
- apiBaseUrl: "http://127.0.0.1:3000"
311
- }
312
- });
313
-
314
- const app = createSingletonApp();
315
- app.instance("mobile.capacitor.adapter.client", {
316
- available: false
317
- });
318
- app.instance("runtime.realtime.client", {
319
- createSocketIoClient(input = {}) {
320
- calls.push(input);
321
- return socket;
322
- },
323
- disconnectSocketIoClient() {}
324
- });
325
-
326
- const provider = new RealtimeClientProvider();
327
- provider.register(app);
328
-
329
- assert.equal(app.make("runtime.realtime.client.socket"), socket);
330
- assert.deepEqual(calls, [
331
- {
332
- url: "",
333
- options: {}
334
- }
335
- ]);
336
- } finally {
337
- if (previousAppConfig === undefined) {
338
- delete globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
339
- } else {
340
- globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY] = previousAppConfig;
341
- }
342
- }
343
- });
344
-
345
- test("RealtimeClientProvider boots socket listeners and disconnects on shutdown", async () => {
346
- const app = createSingletonApp();
347
- const provider = new RealtimeClientProvider();
348
- provider.register(app);
349
-
350
- const handlers = new Map();
351
- const anyHandlers = new Set();
352
- const socket = {
353
- on(event, handler) {
354
- handlers.set(event, handler);
355
- },
356
- off(event, handler) {
357
- if (handlers.get(event) === handler) {
358
- handlers.delete(event);
359
- }
360
- },
361
- onAny(handler) {
362
- anyHandlers.add(handler);
363
- },
364
- offAny(handler) {
365
- anyHandlers.delete(handler);
366
- },
367
- emitEvent(event, payload) {
368
- const handler = handlers.get(event);
369
- if (typeof handler === "function") {
370
- handler(payload);
371
- }
372
- for (const next of anyHandlers) {
373
- next(event, payload);
374
- }
375
- }
376
- };
377
-
378
- let disconnectCalls = 0;
379
- app.instance("runtime.realtime.client", {
380
- createSocketIoClient() {
381
- return socket;
382
- },
383
- disconnectSocketIoClient() {
384
- disconnectCalls += 1;
385
- }
386
- });
387
-
388
- const received = [];
389
- registerRealtimeClientListener(app, "test.realtime.listener", () => ({
390
- listenerId: "test.realtime.listener",
391
- event: "customers.record.changed",
392
- handle({ event, payload }) {
393
- received.push({
394
- event,
395
- payload
396
- });
397
- }
398
- }));
399
-
400
- await provider.boot(app);
401
- socket.emitEvent("customers.record.changed", {
402
- id: 10
403
- });
404
- await Promise.resolve();
405
- await provider.shutdown(app);
406
-
407
- assert.deepEqual(received, [
408
- {
409
- event: "customers.record.changed",
410
- payload: {
411
- id: 10
412
- }
413
- }
414
- ]);
415
- assert.equal(disconnectCalls, 1);
416
- });
417
-
418
- test("RealtimeServiceProvider bridges service event metadata to socket emissions", async () => {
419
- const app = createSingletonApp();
420
- app.instance("jskit.fastify", {
421
- server: createServer()
422
182
  });
423
- app.singleton("authService", () => ({
424
- async authenticateRequest() {
425
- return {
426
- authenticated: false
427
- };
428
- }
429
- }));
430
- app.singleton("internal.repository.workspace-memberships", () => ({
431
- async listActiveWorkspaceIdsByUserId() {
432
- return [];
433
- }
434
- }));
435
- installServiceRegistrationApi(app);
436
- app.singleton("domainEvents", (scope) => createDomainEvents(scope));
437
- app.service(
438
- "test.customers.service",
439
- () => ({
440
- async createRecord() {
441
- return { id: 17, name: "Ada" };
183
+ const inputs = {
184
+ "client.components": {
185
+ register(id, component) {
186
+ registrations.set(id, component);
442
187
  }
443
- }),
444
- {
445
- events: {
446
- createRecord: [
447
- {
448
- type: "entity.changed",
449
- source: "crud",
450
- entity: "record",
451
- operation: "created",
452
- realtime: {
453
- event: "customers.record.changed"
454
- }
455
- }
456
- ]
188
+ },
189
+ "client.env": {},
190
+ "client.logger": logger,
191
+ "client.vue": {
192
+ provide(id, value) {
193
+ provided.set(id, value);
457
194
  }
458
195
  }
459
- );
460
-
461
- const provider = new RealtimeServiceProvider();
462
- provider.register(app);
463
- await provider.boot(app);
464
-
465
- const io = app.make("runtime.realtime.io");
466
- const emitted = [];
467
- io.to = (room) => {
468
- return {
469
- emit(eventName, payload) {
470
- emitted.push({
471
- room,
472
- eventName,
473
- payload
474
- });
475
- return null;
476
- }
477
- };
478
196
  };
479
-
480
- const service = app.make("test.customers.service");
481
- await service.createRecord({
482
- context: {
483
- visibilityContext: {
484
- visibility: "workspace",
485
- scopeOwnerId: 24
486
- }
487
- }
197
+ if (mobile) {
198
+ inputs["client.mobile"] = mobile;
199
+ }
200
+ const runtime = createCapabilityRuntime({
201
+ inputs,
202
+ providers: [RealtimeClientProvider, probe]
488
203
  });
489
- await provider.shutdown(app);
490
-
491
- assert.equal(emitted.length, 1);
492
- assert.equal(emitted[0].room, "workspace:24");
493
- assert.equal(emitted[0].eventName, "customers.record.changed");
494
- assert.equal(emitted[0].payload?.source, "crud");
495
- assert.equal(emitted[0].payload?.operation, "created");
496
- });
204
+ await runtime.start();
205
+ return { provided, realtime, registrations, runtime };
206
+ }
497
207
 
498
- test("RealtimeServiceProvider resolves custom audience callback", async () => {
499
- const app = createSingletonApp();
500
- app.instance("jskit.fastify", {
501
- server: createServer()
502
- });
503
- app.singleton("authService", () => ({
504
- async authenticateRequest() {
505
- return {
506
- authenticated: false
507
- };
508
- }
509
- }));
510
- app.singleton("internal.repository.workspace-memberships", () => ({
511
- async listActiveWorkspaceIdsByUserId() {
512
- return [];
513
- }
514
- }));
515
- installServiceRegistrationApi(app);
516
- app.singleton("domainEvents", (scope) => createDomainEvents(scope));
517
- app.service(
518
- "test.customers.service",
519
- () => ({
520
- async updateRecord() {
521
- return { id: 88 };
522
- }
523
- }),
524
- {
525
- events: {
526
- updateRecord: [
527
- {
528
- type: "entity.changed",
529
- source: "crud",
530
- entity: "record",
531
- operation: "updated",
532
- realtime: {
533
- event: "customers.record.changed",
534
- audience: ({ event }) => ({
535
- userId: event?.actorId
536
- })
537
- }
538
- }
539
- ]
540
- }
541
- }
208
+ test("RealtimeClientProvider publishes one explicit client capability", async () => {
209
+ const fixture = await startRealtimeClient();
210
+ assert.equal(typeof fixture.realtime.createSocketIoClient, "function");
211
+ assert.equal(typeof fixture.realtime.disconnectSocketIoClient, "function");
212
+ assert.equal(fixture.realtime.config.url, "");
213
+ assert.equal(fixture.registrations.has("realtime.web.connection.indicator"), true);
214
+ assert.equal(
215
+ fixture.provided.get("jskit.realtime.runtime.client.socket"),
216
+ fixture.realtime.socket
542
217
  );
543
-
544
- const provider = new RealtimeServiceProvider();
545
- provider.register(app);
546
- await provider.boot(app);
547
-
548
- const io = app.make("runtime.realtime.io");
549
- const emitted = [];
550
- io.to = (room) => {
551
- return {
552
- emit(eventName, payload) {
553
- emitted.push({
554
- room,
555
- eventName,
556
- payload
557
- });
558
- return null;
559
- }
560
- };
561
- };
562
-
563
- const service = app.make("test.customers.service");
564
- await service.updateRecord(
565
- {
566
- id: 88
567
- },
568
- {
569
- context: {
570
- actor: {
571
- id: 9
572
- }
573
- }
574
- }
575
- );
576
- await provider.shutdown(app);
577
-
578
- assert.equal(emitted.length, 1);
579
- assert.equal(emitted[0].room, "user:9");
580
- assert.equal(emitted[0].eventName, "customers.record.changed");
581
- assert.equal(emitted[0].payload?.operation, "updated");
218
+ await fixture.runtime.shutdown();
582
219
  });
583
220
 
584
- test("RealtimeServiceProvider merges custom realtime payload with canonical domain event fields", async () => {
585
- const app = createSingletonApp();
586
- app.instance("jskit.fastify", {
587
- server: createServer()
588
- });
589
- app.singleton("authService", () => ({
590
- async authenticateRequest() {
591
- return {
592
- authenticated: false
593
- };
594
- }
595
- }));
596
- app.singleton("internal.repository.workspace-memberships", () => ({
597
- async listActiveWorkspaceIdsByUserId() {
598
- return [];
599
- }
600
- }));
601
- installServiceRegistrationApi(app);
602
- app.singleton("domainEvents", (scope) => createDomainEvents(scope));
603
- app.service(
604
- "test.workspace.service",
605
- () => ({
606
- async updateWorkspace() {
607
- return { id: 11, slug: "acme" };
608
- }
609
- }),
610
- {
611
- events: {
612
- updateWorkspace: [
613
- {
614
- type: "entity.changed",
615
- source: "workspace",
616
- entity: "settings",
617
- operation: "updated",
618
- action: "settings-saved",
619
- reason: "profile-update",
620
- realtime: {
621
- event: "workspace.settings.changed",
622
- payload: ({ result }) => ({
623
- workspaceSlug: result?.slug || ""
624
- }),
625
- audience: "event_scope"
626
- }
627
- }
628
- ]
629
- }
630
- }
631
- );
632
-
633
- const provider = new RealtimeServiceProvider();
634
- provider.register(app);
635
- await provider.boot(app);
636
-
637
- const io = app.make("runtime.realtime.io");
638
- const emitted = [];
639
- io.to = (room) => {
640
- return {
641
- emit(eventName, payload) {
642
- emitted.push({
643
- room,
644
- eventName,
645
- payload
646
- });
647
- return null;
648
- }
649
- };
650
- };
651
-
652
- const service = app.make("test.workspace.service");
653
- await service.updateWorkspace(
654
- {
655
- id: 11,
656
- slug: "acme"
657
- },
658
- {
659
- context: {
660
- visibilityContext: {
661
- visibility: "workspace",
662
- scopeOwnerId: 11
663
- },
664
- actor: {
665
- id: 4
666
- }
667
- }
221
+ for (const [name, available, expectedUrl] of [
222
+ ["uses the configured API URL inside Capacitor", true, "http://127.0.0.1:3000"],
223
+ ["keeps web socket connections URL-less outside Capacitor", false, ""]
224
+ ]) {
225
+ test(`RealtimeClientProvider ${name}`, async () => {
226
+ const previousAppConfig = globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
227
+ try {
228
+ setClientAppConfig({ mobile: { enabled: true, apiBaseUrl: "http://127.0.0.1:3000" } });
229
+ const fixture = await startRealtimeClient({
230
+ mobile: { adapter: { available } }
231
+ });
232
+ assert.equal(fixture.realtime.config.url, expectedUrl);
233
+ await fixture.runtime.shutdown();
234
+ } finally {
235
+ if (previousAppConfig === undefined) delete globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
236
+ else globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY] = previousAppConfig;
668
237
  }
669
- );
670
- await provider.shutdown(app);
671
-
672
- assert.equal(emitted.length, 1);
673
- assert.equal(emitted[0].room, "workspace:11");
674
- assert.equal(emitted[0].eventName, "workspace.settings.changed");
675
- assert.equal(emitted[0].payload?.action, "settings-saved");
676
- assert.equal(emitted[0].payload?.reason, "profile-update");
677
- assert.equal(emitted[0].payload?.workspaceSlug, "acme");
678
- assert.equal(emitted[0].payload?.source, "workspace");
679
- assert.equal(emitted[0].payload?.entity, "settings");
680
- assert.equal(emitted[0].payload?.operation, "updated");
681
- assert.equal(emitted[0].payload?.meta?.action, "settings-saved");
682
- assert.equal(emitted[0].payload?.meta?.reason, "profile-update");
683
- assert.equal(emitted[0].payload?.scope?.kind, "workspace");
684
- assert.equal(emitted[0].payload?.scope?.id, "11");
685
- });
686
-
687
- test("RealtimeServiceProvider emits only the matching dispatcher event for each service method event", async () => {
688
- const app = createSingletonApp();
689
- app.instance("jskit.fastify", {
690
- server: createServer()
691
238
  });
692
- app.singleton("authService", () => ({
693
- async authenticateRequest() {
694
- return {
695
- authenticated: false
696
- };
697
- }
698
- }));
699
- app.singleton("internal.repository.workspace-memberships", () => ({
700
- async listActiveWorkspaceIdsByUserId() {
701
- return [];
702
- }
703
- }));
704
- installServiceRegistrationApi(app);
705
- app.singleton("domainEvents", (scope) => createDomainEvents(scope));
706
- app.service(
707
- "test.workspace.settings.service",
708
- () => ({
709
- async updateSettings() {
710
- return { id: 11 };
711
- }
712
- }),
713
- {
714
- events: {
715
- updateSettings: [
716
- {
717
- type: "entity.changed",
718
- source: "workspace",
719
- entity: "settings",
720
- operation: "updated",
721
- realtime: {
722
- event: "workspace.settings.changed",
723
- audience: "event_scope"
724
- }
725
- },
726
- {
727
- type: "entity.changed",
728
- source: "users",
729
- entity: "bootstrap",
730
- operation: "updated",
731
- realtime: {
732
- event: "users.bootstrap.changed",
733
- audience: "event_scope"
734
- }
735
- }
736
- ]
737
- }
738
- }
739
- );
740
-
741
- const provider = new RealtimeServiceProvider();
742
- provider.register(app);
743
- await provider.boot(app);
744
-
745
- const io = app.make("runtime.realtime.io");
746
- const emitted = [];
747
- io.to = (room) => {
748
- return {
749
- emit(eventName, payload) {
750
- emitted.push({
751
- room,
752
- eventName,
753
- payload
754
- });
755
- return null;
756
- }
757
- };
758
- };
759
-
760
- const service = app.make("test.workspace.settings.service");
761
- await service.updateSettings(
762
- { id: 11 },
763
- {
764
- context: {
765
- actor: {
766
- id: 4
767
- },
768
- visibilityContext: {
769
- visibility: "workspace",
770
- scopeOwnerId: 11
771
- }
772
- }
773
- }
774
- );
775
- await provider.shutdown(app);
776
-
777
- assert.equal(emitted.length, 2);
778
- assert.deepEqual(
779
- emitted.map((entry) => entry.eventName).sort(),
780
- ["users.bootstrap.changed", "workspace.settings.changed"]
781
- );
782
- });
239
+ }