@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.
- package/package.json +25 -69
- package/patterns/realtime-application/PATTERN.md +69 -0
- package/patterns/realtime-application/example/.env.example +1 -0
- package/patterns/realtime-application/example/package.json +8 -0
- package/patterns/realtime-application/example/src/placement.js +17 -0
- package/src/client/RealtimeClientProvider.js +81 -203
- package/src/server/RealtimeProvider.js +98 -0
- package/src/server/realtimeAudience.js +220 -0
- package/src/server/realtimeDelivery.js +41 -0
- package/test/entrypoints.boundary.test.js +6 -12
- package/test/providerRuntime.test.js +187 -730
- package/test/realtimeApplicationPattern.test.js +16 -0
- package/src/client/listeners.js +0 -68
- package/src/server/RealtimeServiceProvider.js +0 -764
- package/test/clientListeners.test.js +0 -66
|
@@ -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
|
|
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 {
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
51
|
-
const
|
|
52
|
-
const singletons = new Map();
|
|
53
|
-
const tags = new Map();
|
|
14
|
+
function createIoDouble() {
|
|
15
|
+
const emitted = [];
|
|
54
16
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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("
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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("
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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("
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
490
|
-
|
|
491
|
-
|
|
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("
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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
|
+
}
|