@jskit-ai/realtime 0.1.177 → 0.1.178
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/realtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.178",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"socket.io-client": "^4.8.3"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@jskit-ai/kernel": "0.1.
|
|
24
|
-
"@jskit-ai/shell-web": "0.1.
|
|
23
|
+
"@jskit-ai/kernel": "0.1.181",
|
|
24
|
+
"@jskit-ai/shell-web": "0.1.185",
|
|
25
25
|
"vue": "^3.5.13"
|
|
26
26
|
},
|
|
27
27
|
"description": "Thin, generic realtime runtime wrappers for socket.io server and client.",
|
|
@@ -63,28 +63,25 @@ const RealtimeProvider = defineProvider({
|
|
|
63
63
|
provides: {
|
|
64
64
|
realtime: "runtime.realtime"
|
|
65
65
|
},
|
|
66
|
-
setup({
|
|
66
|
+
setup({ config, env, fastify, logger }) {
|
|
67
67
|
const io = createSocketIoServer({ fastify });
|
|
68
68
|
const providerLogger = createProviderLogger(logger, { debugEnabled: debugEnabled(config, env) });
|
|
69
|
-
const delivery = createRealtimeDelivery({ io, database, logger: providerLogger, authService, workspaces });
|
|
70
69
|
const realtime = createRealtimeCapability({ io });
|
|
71
70
|
stateByCapability.set(realtime, {
|
|
72
71
|
authenticationRequired: false,
|
|
73
|
-
delivery,
|
|
72
|
+
delivery: null,
|
|
74
73
|
io,
|
|
75
74
|
providerLogger,
|
|
76
75
|
redisConnection: null
|
|
77
76
|
});
|
|
78
|
-
events.register({
|
|
79
|
-
id: "runtime.realtime.delivery",
|
|
80
|
-
matches: (event) => Boolean(normalizeText(event?.realtime?.event)),
|
|
81
|
-
handle: delivery.handle
|
|
82
|
-
});
|
|
83
77
|
return { realtime };
|
|
84
78
|
},
|
|
85
|
-
async boot({ authService, env, workspaces }, { outputs }) {
|
|
79
|
+
async boot({ authService, database, env, events, workspaces }, { outputs }) {
|
|
86
80
|
const state = stateByCapability.get(outputs.realtime);
|
|
87
81
|
if (!state) throw new Error("Realtime runtime state is unavailable.");
|
|
82
|
+
state.delivery = createRealtimeDelivery({
|
|
83
|
+
io: state.io, database, logger: state.providerLogger, authService, workspaces
|
|
84
|
+
});
|
|
88
85
|
state.authenticationRequired = realtimeAuthenticationRequired(authService);
|
|
89
86
|
registerSocketAudienceBootstrap({
|
|
90
87
|
io: state.io,
|
|
@@ -98,11 +95,16 @@ const RealtimeProvider = defineProvider({
|
|
|
98
95
|
redisNamespace: resolveRealtimeRedisNamespace(env)
|
|
99
96
|
});
|
|
100
97
|
state.delivery.start({ redisConfigured: state.redisConnection.enabled });
|
|
98
|
+
events.register({
|
|
99
|
+
id: "runtime.realtime.delivery",
|
|
100
|
+
matches: (event) => Boolean(normalizeText(event?.realtime?.event)),
|
|
101
|
+
handle: state.delivery.handle
|
|
102
|
+
});
|
|
101
103
|
},
|
|
102
104
|
async shutdown(_dependencies, { outputs }) {
|
|
103
105
|
const state = stateByCapability.get(outputs.realtime);
|
|
104
106
|
if (!state) return;
|
|
105
|
-
state.delivery
|
|
107
|
+
state.delivery?.stop();
|
|
106
108
|
await closeSocketIoServer(state.io);
|
|
107
109
|
await closeSocketIoRedisConnections(state.redisConnection || {});
|
|
108
110
|
stateByCapability.delete(outputs.realtime);
|
|
@@ -26,6 +26,17 @@ function roomForWorkspaceUser(workspaceId, userId) {
|
|
|
26
26
|
return `workspace:${String(workspaceId || "").trim()}:user:${String(userId || "").trim()}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function workspaceAudienceRooms(workspaceIds, actorId) {
|
|
30
|
+
const rooms = [];
|
|
31
|
+
for (const rawId of normalizeArray(workspaceIds)) {
|
|
32
|
+
const workspaceId = normalizeRecordId(rawId, { fallback: null });
|
|
33
|
+
if (workspaceId) {
|
|
34
|
+
rooms.push(roomForWorkspace(workspaceId), roomForWorkspaceUser(workspaceId, actorId));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return rooms;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
function resolveScopeWorkspaceId(scope = {}) {
|
|
30
41
|
const kind = normalizeText(scope?.kind).toLowerCase();
|
|
31
42
|
if (kind === "workspace") return normalizeRecordId(scope.id, { fallback: null });
|
|
@@ -242,11 +253,8 @@ function registerSocketAudienceBootstrap({ io, logger, authService = null, works
|
|
|
242
253
|
const workspaceIds = typeof repository?.listActiveWorkspaceIdsByUserId === "function"
|
|
243
254
|
? await repository.listActiveWorkspaceIdsByUserId(actorId)
|
|
244
255
|
: [];
|
|
245
|
-
for (const
|
|
246
|
-
|
|
247
|
-
if (!workspaceId) continue;
|
|
248
|
-
socket.join(roomForWorkspace(workspaceId));
|
|
249
|
-
socket.join(roomForWorkspaceUser(workspaceId, actorId));
|
|
256
|
+
for (const room of workspaceAudienceRooms(workspaceIds, actorId)) {
|
|
257
|
+
socket.join(room);
|
|
250
258
|
}
|
|
251
259
|
} catch (error) {
|
|
252
260
|
logger.warn({ error: String(error?.message || error) }, "Realtime socket audience bootstrap failed.");
|
|
@@ -262,21 +270,18 @@ async function revalidateSocket({ socket, authService, workspaces = null }) {
|
|
|
262
270
|
socket.disconnect(true);
|
|
263
271
|
return null;
|
|
264
272
|
}
|
|
265
|
-
if (actor)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (room.startsWith("workspace:") && !rooms.has(room)) await socket.leave(room);
|
|
276
|
-
}
|
|
277
|
-
for (const room of rooms) await socket.join(room);
|
|
278
|
-
}
|
|
273
|
+
if (!actor) return null;
|
|
274
|
+
|
|
275
|
+
rememberSocketActorId(socket, actor.id);
|
|
276
|
+
const repository = workspaces?.repositories?.workspaceMemberships;
|
|
277
|
+
if (typeof repository?.listActiveWorkspaceIdsByUserId !== "function") return actor;
|
|
278
|
+
|
|
279
|
+
const workspaceIds = await repository.listActiveWorkspaceIdsByUserId(actor.id);
|
|
280
|
+
const rooms = new Set(workspaceAudienceRooms(workspaceIds, actor.id));
|
|
281
|
+
for (const room of socket.rooms) {
|
|
282
|
+
if (room.startsWith("workspace:") && !rooms.has(room)) await socket.leave(room);
|
|
279
283
|
}
|
|
284
|
+
for (const room of rooms) await socket.join(room);
|
|
280
285
|
return actor;
|
|
281
286
|
}
|
|
282
287
|
|
|
@@ -6,11 +6,14 @@ const DELIVERY_EVENT = "jskit:realtime:delivery";
|
|
|
6
6
|
function publicRealtimePayload(event) {
|
|
7
7
|
const { realtime } = event && typeof event === "object" ? event : {};
|
|
8
8
|
const canonical = {};
|
|
9
|
-
for (const key of [
|
|
9
|
+
for (const key of [
|
|
10
|
+
"type", "source", "entity", "operation", "entityId", "scope", "actorId",
|
|
11
|
+
"commandId", "sourceClientId", "occurredAt"
|
|
12
|
+
]) {
|
|
10
13
|
if (Object.hasOwn(event, key)) canonical[key] = event[key];
|
|
11
14
|
}
|
|
12
15
|
const payload = realtime?.payload;
|
|
13
|
-
if (payload != null && (
|
|
16
|
+
if (payload != null && (typeof payload !== "object" || Array.isArray(payload))) {
|
|
14
17
|
throw new TypeError("Realtime event payload must be an object when provided.");
|
|
15
18
|
}
|
|
16
19
|
return Object.freeze({ ...(payload || {}), ...canonical });
|
|
@@ -35,7 +38,7 @@ function createRealtimeDelivery({ io, database = null, logger, authService = nul
|
|
|
35
38
|
let revalidationTimer = null;
|
|
36
39
|
let stopped = false;
|
|
37
40
|
|
|
38
|
-
async function
|
|
41
|
+
async function revalidate(socket) {
|
|
39
42
|
try {
|
|
40
43
|
return await revalidateSocket({ socket, authService, workspaces });
|
|
41
44
|
} catch (error) {
|
|
@@ -48,11 +51,16 @@ function createRealtimeDelivery({ io, database = null, logger, authService = nul
|
|
|
48
51
|
async function deliverLocal({ eventName, payload, targets }) {
|
|
49
52
|
await Promise.all([...io.sockets.sockets.values()].map(async (socket) => {
|
|
50
53
|
try {
|
|
51
|
-
const actor = await
|
|
54
|
+
const actor = await revalidate(socket);
|
|
52
55
|
if (!socket.connected) return;
|
|
53
56
|
if (!targets.broadcastAllClients && !targets.rooms.some((room) => socket.rooms.has(room))) return;
|
|
54
|
-
if (typeof authService.realtime?.authorizeEvent === "function"
|
|
55
|
-
|
|
57
|
+
if (typeof authService.realtime?.authorizeEvent === "function") {
|
|
58
|
+
const authorized = await authService.realtime.authorizeEvent({
|
|
59
|
+
actor,
|
|
60
|
+
event: { name: eventName, payload }
|
|
61
|
+
});
|
|
62
|
+
if (authorized !== true) return;
|
|
63
|
+
}
|
|
56
64
|
if (socket.connected) socket.emit(eventName, payload);
|
|
57
65
|
} catch (error) {
|
|
58
66
|
logger.warn({ socketEvent: eventName, error: String(error?.message || error) }, "Realtime delivery denied after authorization failure.");
|
|
@@ -71,7 +79,7 @@ function createRealtimeDelivery({ io, database = null, logger, authService = nul
|
|
|
71
79
|
function scheduleRevalidation() {
|
|
72
80
|
if (stopped) return;
|
|
73
81
|
revalidationTimer = setTimeout(async () => {
|
|
74
|
-
await Promise.all([...io.sockets.sockets.values()].map(
|
|
82
|
+
await Promise.all([...io.sockets.sockets.values()].map(revalidate));
|
|
75
83
|
scheduleRevalidation();
|
|
76
84
|
}, 30_000);
|
|
77
85
|
revalidationTimer.unref();
|
|
@@ -8,9 +8,37 @@ import { RealtimeClientProvider } from "../src/client/RealtimeClientProvider.js"
|
|
|
8
8
|
import { RealtimeProvider } from "../src/server/RealtimeProvider.js";
|
|
9
9
|
import { registerSocketAudienceBootstrap } from "../src/server/realtimeAudience.js";
|
|
10
10
|
import { createRealtimeDelivery } from "../src/server/realtimeDelivery.js";
|
|
11
|
+
import { attachDeferredAuthService, createDeferredAuthService } from "../../auth-core/src/server/deferredAuthService.js";
|
|
11
12
|
|
|
12
13
|
const logger = Object.freeze({ debug() {}, info() {}, warn() {}, error() {} });
|
|
13
14
|
|
|
15
|
+
test("RealtimeProvider waits for authentication boot before accessing the auth service", async () => {
|
|
16
|
+
const authService = createDeferredAuthService();
|
|
17
|
+
const authProvider = defineProvider({
|
|
18
|
+
id: "test.deferred.auth",
|
|
19
|
+
provides: { authService: "auth.service" },
|
|
20
|
+
setup() { return { authService }; },
|
|
21
|
+
boot() {
|
|
22
|
+
attachDeferredAuthService(authService, {
|
|
23
|
+
realtime: { requireAuthentication: true },
|
|
24
|
+
async authenticateRequest() { return { authenticated: false, actor: null }; }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const runtime = createCapabilityRuntime({
|
|
29
|
+
inputs: {
|
|
30
|
+
"runtime.config": {}, "runtime.env": {},
|
|
31
|
+
"runtime.fastify": { server: createServer() }, "runtime.logger": logger
|
|
32
|
+
},
|
|
33
|
+
providers: [EventProvider, RealtimeProvider, authProvider]
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
await runtime.start();
|
|
37
|
+
} finally {
|
|
38
|
+
await runtime.shutdown();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
14
42
|
function createIoDouble() {
|
|
15
43
|
const emitted = [];
|
|
16
44
|
return {
|