@jskit-ai/realtime 0.1.156 → 0.1.158

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,22 +1,21 @@
1
1
  {
2
2
  "name": "@jskit-ai/realtime",
3
- "version": "0.1.156",
3
+ "version": "0.1.158",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "exports": {
9
- "./server/RealtimeServiceProvider": "./src/server/RealtimeServiceProvider.js",
9
+ "./server/RealtimeProvider": "./src/server/RealtimeProvider.js",
10
10
  "./server/runtime": "./src/server/runtime.js",
11
11
  "./client": "./src/client/RealtimeClientProvider.js",
12
12
  "./client/RealtimeClientProvider": "./src/client/RealtimeClientProvider.js",
13
- "./client/listeners": "./src/client/listeners.js",
14
13
  "./client/composables/useRealtimeEvent": "./src/client/composables/useRealtimeEvent.js",
15
14
  "./client/runtime": "./src/client/runtime.js"
16
15
  },
17
16
  "dependencies": {
18
- "@jskit-ai/kernel": "0.1.159",
19
- "@jskit-ai/shell-web": "0.1.163",
17
+ "@jskit-ai/kernel": "0.1.161",
18
+ "@jskit-ai/shell-web": "0.1.165",
20
19
  "@socket.io/redis-adapter": "^8.3.0",
21
20
  "redis": "^5.8.2",
22
21
  "socket.io": "^4.8.3",
@@ -28,30 +27,25 @@
28
27
  "description": "Thin, generic realtime runtime wrappers for socket.io server and client.",
29
28
  "jskit": {
30
29
  "kind": "runtime",
31
- "options": {
32
- "realtime-redis-url": {
33
- "required": true,
34
- "allowEmpty": true,
35
- "values": [],
36
- "defaultValue": "",
37
- "promptLabel": "Realtime Redis URL",
38
- "promptHint": "Leave empty to use in-memory socket adapter."
39
- }
40
- },
41
30
  "capabilities": {
42
31
  "provides": [
43
32
  "runtime.realtime",
44
33
  "runtime.realtime.client"
45
34
  ],
46
- "requires": []
35
+ "requires": [
36
+ "runtime.config",
37
+ "runtime.env",
38
+ "runtime.events",
39
+ "runtime.fastify",
40
+ "runtime.logger"
41
+ ]
47
42
  },
48
43
  "runtime": {
49
44
  "server": {
50
- "providerEntrypoint": "src/server/RealtimeServiceProvider.js",
51
45
  "providers": [
52
46
  {
53
- "entrypoint": "src/server/RealtimeServiceProvider.js",
54
- "export": "RealtimeServiceProvider"
47
+ "entrypoint": "src/server/RealtimeProvider.js",
48
+ "export": "RealtimeProvider"
55
49
  }
56
50
  ]
57
51
  },
@@ -65,33 +59,34 @@
65
59
  }
66
60
  },
67
61
  "metadata": {
62
+ "client": {
63
+ "optimizeDeps": {
64
+ "include": [
65
+ "socket.io-client"
66
+ ]
67
+ }
68
+ },
68
69
  "apiSummary": {
69
70
  "surfaces": [
70
71
  {
71
72
  "subpath": "./server",
72
- "summary": "Exports RealtimeServiceProvider only."
73
+ "summary": "Exports the explicit RealtimeProvider capability provider."
73
74
  },
74
75
  {
75
76
  "subpath": "./client",
76
77
  "summary": "Exports RealtimeClientProvider only."
77
78
  },
78
- {
79
- "subpath": "./client/listeners",
80
- "summary": "Exports client listener registration helpers for provider-level realtime subscriptions."
81
- },
82
79
  {
83
80
  "subpath": "./client/composables/*",
84
81
  "summary": "Exports component-level realtime socket composables."
85
82
  }
86
83
  ],
87
- "containerTokens": {
84
+ "capabilities": {
88
85
  "server": [
89
- "runtime.realtime",
90
- "runtime.realtime.io"
86
+ "runtime.realtime"
91
87
  ],
92
88
  "client": [
93
- "runtime.realtime.client",
94
- "runtime.realtime.client.socket",
89
+ "client.realtime",
95
90
  "realtime.web.connection.indicator"
96
91
  ]
97
92
  }
@@ -109,7 +104,7 @@
109
104
  ],
110
105
  "order": 950,
111
106
  "componentToken": "realtime.web.connection.indicator",
112
- "source": "mutations.text#realtime-placement-indicator"
107
+ "source": "patterns/realtime-application/example/src/placement.js"
113
108
  }
114
109
  ]
115
110
  }
@@ -122,45 +117,6 @@
122
117
  "ws": true
123
118
  }
124
119
  }
125
- },
126
- "mutations": {
127
- "dependencies": {
128
- "runtime": {
129
- "@jskit-ai/kernel": "0.1.159",
130
- "@jskit-ai/shell-web": "0.1.163",
131
- "@socket.io/redis-adapter": "^8.3.0",
132
- "redis": "^5.8.2",
133
- "socket.io": "^4.8.3",
134
- "socket.io-client": "^4.8.3"
135
- },
136
- "dev": {}
137
- },
138
- "packageJson": {
139
- "scripts": {}
140
- },
141
- "procfile": {},
142
- "files": [],
143
- "text": [
144
- {
145
- "file": ".env",
146
- "op": "upsert-env",
147
- "key": "REALTIME_REDIS_URL",
148
- "value": "${option:realtime-redis-url}",
149
- "reason": "Configure optional Redis backplane URL for realtime socket adapter.",
150
- "category": "runtime-config",
151
- "id": "realtime-redis-url"
152
- },
153
- {
154
- "op": "append-text",
155
- "file": "src/placement.js",
156
- "position": "bottom",
157
- "skipIfContains": "id: \"realtime.connection.indicator\"",
158
- "value": "\naddPlacement({\n id: \"realtime.connection.indicator\",\n target: \"shell.status\",\n kind: \"component\",\n surfaces: [\"*\"],\n order: 950,\n componentToken: \"realtime.web.connection.indicator\"\n});\n",
159
- "reason": "Append realtime connection indicator placement into app-owned placement registry.",
160
- "category": "realtime-web",
161
- "id": "realtime-placement-indicator"
162
- }
163
- ]
164
120
  }
165
121
  }
166
122
  }
@@ -0,0 +1,69 @@
1
+ ---
2
+ id: realtime/realtime-application
3
+ title: Realtime application
4
+ summary: Add JSKIT realtime events with an optional Redis backplane and an explicit shell status contribution.
5
+ keywords: redis, realtime, socket.io, sockets, status, websocket
6
+ requires: @jskit-ai/realtime, @jskit-ai/shell-web
7
+ ---
8
+
9
+ # Realtime application
10
+
11
+ ## Use when
12
+
13
+ Use this pattern when the product needs server-to-client events or live query
14
+ refresh. Install the realtime package normally, then add only the product's
15
+ listeners and desired shell contribution.
16
+
17
+ ## Do not use when
18
+
19
+ Do not add realtime merely to poll less often, or before event ownership and
20
+ delivery semantics are clear. A single-process application does not need Redis.
21
+
22
+ ## Product decisions
23
+
24
+ Decide which events are public contracts, their scopes and payloads, reconnect
25
+ behavior, whether a connection indicator belongs in the UI, and whether the
26
+ deployment needs a Redis backplane.
27
+
28
+ ## Invariants
29
+
30
+ - Empty `REALTIME_REDIS_URL` means the in-process adapter.
31
+ - Redis credentials stay outside Git.
32
+ - Listeners are registered through the public provider seams.
33
+ - The status indicator is an explicit app placement, not a source mutation.
34
+ - Event payloads do not become an undocumented second API.
35
+
36
+ ## Framework APIs
37
+
38
+ Server features declare their successful domain events on their actions. The
39
+ installed `RealtimeProvider` delivers only events with an explicit realtime
40
+ name and audience. Client features use `RealtimeClientProvider` and the public
41
+ listener registration helpers from `@jskit-ai/realtime`. Use the normal shell
42
+ placement registry for the optional status component.
43
+
44
+ ## Example files
45
+
46
+ `example/package.json` declares realtime and shell runtime packages.
47
+ `example/.env.example` documents the optional Redis input.
48
+ `example/src/placement.js` adds the standard connection indicator explicitly.
49
+
50
+ ## Variation points
51
+
52
+ Omit the placement when the product does not need a visible connection state.
53
+ Change event listeners, Redis provisioning, and client invalidation behavior to
54
+ match the product. Keep transport retry policy in the realtime runtime.
55
+
56
+ ## Verification
57
+
58
+ - Test in-process delivery without Redis.
59
+ - When Redis is selected, test delivery across two server processes.
60
+ - Disconnect and reconnect a browser and verify recovery behavior.
61
+ - Confirm the status contribution renders in compact and expanded shells.
62
+
63
+ ## Avoid
64
+
65
+ - prompting for optional Redis configuration during package installation
66
+ - appending source into placement files
67
+ - leaking Redis values to client config
68
+ - generic “data changed” payloads with no scope
69
+ - receipts, provenance, or mutation history
@@ -0,0 +1 @@
1
+ REALTIME_REDIS_URL=
@@ -0,0 +1,8 @@
1
+ {
2
+ "private": true,
3
+ "type": "module",
4
+ "dependencies": {
5
+ "@jskit-ai/realtime": "0.1.158",
6
+ "@jskit-ai/shell-web": "0.1.165"
7
+ }
8
+ }
@@ -0,0 +1,17 @@
1
+ import { createPlacementRegistry } from "@jskit-ai/shell-web/client/placement";
2
+
3
+ const registry = createPlacementRegistry();
4
+ const { addPlacement } = registry;
5
+
6
+ addPlacement({
7
+ id: "realtime.connection.indicator",
8
+ target: "shell.status",
9
+ kind: "component",
10
+ surfaces: ["*"],
11
+ order: 950,
12
+ componentToken: "realtime.web.connection.indicator"
13
+ });
14
+
15
+ export default function getPlacements() {
16
+ return registry.build();
17
+ }
@@ -1,37 +1,22 @@
1
- import { createSocketIoClient, disconnectSocketIoClient } from "./runtime.js";
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
2
2
  import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { createProviderLogger as createSharedProviderLogger } from "@jskit-ai/kernel/shared/support/providerLogger";
3
+ import { createProviderLogger } from "@jskit-ai/kernel/shared/support/providerLogger";
4
4
  import { getClientAppConfig, resolveClientBootstrapDebugEnabled, resolveMobileConfig } from "@jskit-ai/kernel/client";
5
5
  import RealtimeConnectionIndicator from "./components/RealtimeConnectionIndicator.js";
6
- import { resolveRealtimeClientListeners } from "./listeners.js";
6
+ import { createSocketIoClient, disconnectSocketIoClient } from "./runtime.js";
7
7
 
8
8
  const REALTIME_RUNTIME_CLIENT_API = Object.freeze({
9
9
  createSocketIoClient,
10
10
  disconnectSocketIoClient
11
11
  });
12
12
 
13
- function isCapacitorRuntimeAvailable(app) {
14
- if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
15
- return false;
16
- }
17
- if (app.has("mobile.capacitor.adapter.client") !== true) {
18
- return false;
19
- }
20
-
21
- const adapter = app.make("mobile.capacitor.adapter.client");
22
- return adapter?.available === true;
23
- }
24
-
25
- function resolveRealtimeClientConfig(app) {
13
+ function resolveRealtimeClientConfig({ env = {}, mobile = null } = {}) {
26
14
  const appConfig = normalizeObject(getClientAppConfig());
27
- const env = app && typeof app.has === "function" && app.has("jskit.client.env") ? normalizeObject(app.make("jskit.client.env")) : {};
28
15
  const realtime = normalizeObject(appConfig.realtime);
29
16
  const realtimeClient = normalizeObject(appConfig.realtimeClient);
30
- const mobileConfig = resolveMobileConfig({
31
- mobile: normalizeObject(appConfig.mobile)
32
- });
17
+ const mobileConfig = resolveMobileConfig({ mobile: normalizeObject(appConfig.mobile) });
33
18
  const url = normalizeText(
34
- realtimeClient.url || (mobileConfig.enabled === true && isCapacitorRuntimeAvailable(app) ? mobileConfig.apiBaseUrl : "")
19
+ realtimeClient.url || (mobileConfig.enabled === true && mobile?.adapter?.available === true ? mobileConfig.apiBaseUrl : "")
35
20
  );
36
21
  const options = normalizeObject(realtimeClient.options);
37
22
  const explicitDebugEnabled =
@@ -40,7 +25,7 @@ function resolveRealtimeClientConfig(app) {
40
25
  : typeof realtime.debug === "boolean"
41
26
  ? realtime.debug
42
27
  : undefined;
43
- const hasRealtimeDebugEnvOverride = Object.hasOwn(env, "VITE_REALTIME_DEBUG");
28
+ const hasRealtimeDebugEnvOverride = Object.hasOwn(normalizeObject(env), "VITE_REALTIME_DEBUG");
44
29
  const debugEnabled = hasRealtimeDebugEnvOverride
45
30
  ? resolveClientBootstrapDebugEnabled({
46
31
  env,
@@ -53,221 +38,114 @@ function resolveRealtimeClientConfig(app) {
53
38
  debugEnvKey: "VITE_REALTIME_DEBUG"
54
39
  });
55
40
 
56
- return Object.freeze({
57
- url,
58
- options,
59
- debugEnabled
60
- });
41
+ return Object.freeze({ url, options, debugEnabled });
61
42
  }
62
43
 
63
- class RealtimeClientProvider {
64
- static id = "runtime.realtime.client";
65
-
66
- register(app) {
67
- if (!app || typeof app.singleton !== "function") {
68
- throw new Error("RealtimeClientProvider requires application singleton().");
69
- }
70
-
71
- app.singleton("runtime.realtime.client", () => REALTIME_RUNTIME_CLIENT_API);
72
- app.singleton("realtime.web.connection.indicator", () => RealtimeConnectionIndicator);
73
- app.singleton("runtime.realtime.client.socket", (scope) => {
74
- const realtimeRuntime = scope.make("runtime.realtime.client");
75
- const realtimeClientConfig = resolveRealtimeClientConfig(scope);
76
- return realtimeRuntime.createSocketIoClient({
77
- url: realtimeClientConfig.url,
78
- options: realtimeClientConfig.options
79
- });
80
- });
81
- }
44
+ function createRealtimeClient({ config, loggerInput } = {}) {
45
+ const logger = createProviderLogger(loggerInput, {
46
+ debugEnabled: config.debugEnabled
47
+ });
48
+ const socket = createSocketIoClient({
49
+ url: config.url,
50
+ options: config.options
51
+ });
52
+ const detach = [];
53
+ let initialized = false;
82
54
 
83
- boot(app) {
84
- if (!app || typeof app.make !== "function") {
85
- throw new Error("RealtimeClientProvider requires application make().");
55
+ function initialize(vueApp = null) {
56
+ if (initialized) {
57
+ return;
86
58
  }
87
-
88
- const realtimeClientConfig = resolveRealtimeClientConfig(app);
89
- const logger = createSharedProviderLogger(app, {
90
- debugEnabled: realtimeClientConfig.debugEnabled
91
- });
92
- const socket = app.make("runtime.realtime.client.socket");
93
- const listeners = resolveRealtimeClientListeners(app);
94
- const detach = [];
95
-
96
- logger.debug(
97
- {
98
- providerId: RealtimeClientProvider.id,
99
- listenerCount: listeners.length,
100
- listeners: listeners.map((listener) => ({
101
- listenerId: listener.listenerId,
102
- event: listener.event
103
- }))
104
- },
105
- "Realtime client booted listeners."
106
- );
59
+ initialized = true;
107
60
 
108
61
  if (typeof socket.on === "function") {
109
- const onConnect = () => {
110
- logger.debug(
111
- {
112
- providerId: RealtimeClientProvider.id,
113
- socketConnected: true
114
- },
115
- "Realtime client socket connected."
116
- );
117
- };
118
- const onDisconnect = (reason) => {
62
+ const onConnect = () => logger.debug({ socketConnected: true }, "Realtime client socket connected.");
63
+ const onDisconnect = (reason) =>
119
64
  logger.debug(
120
- {
121
- providerId: RealtimeClientProvider.id,
122
- socketConnected: false,
123
- reason: String(reason || "")
124
- },
65
+ { socketConnected: false, reason: String(reason || "") },
125
66
  "Realtime client socket disconnected."
126
67
  );
127
- };
128
- const onConnectError = (error) => {
68
+ const onConnectError = (error) =>
129
69
  logger.warn(
130
- {
131
- providerId: RealtimeClientProvider.id,
132
- error: String(error?.message || error || "unknown error")
133
- },
70
+ { error: String(error?.message || error || "unknown error") },
134
71
  "Realtime client socket connect error."
135
72
  );
136
- };
137
73
 
138
74
  socket.on("connect", onConnect);
139
75
  socket.on("disconnect", onDisconnect);
140
76
  socket.on("connect_error", onConnectError);
141
77
  detach.push(() => {
142
- if (typeof socket.off === "function") {
143
- socket.off("connect", onConnect);
144
- socket.off("disconnect", onDisconnect);
145
- socket.off("connect_error", onConnectError);
146
- }
78
+ socket.off?.("connect", onConnect);
79
+ socket.off?.("disconnect", onDisconnect);
80
+ socket.off?.("connect_error", onConnectError);
147
81
  });
148
82
 
149
- if (realtimeClientConfig.debugEnabled === true && typeof socket.onAny === "function") {
150
- const onAnyDebug = (eventName, payload) => {
83
+ if (config.debugEnabled === true && typeof socket.onAny === "function") {
84
+ const onAny = (eventName, payload) =>
151
85
  logger.debug(
152
86
  {
153
- providerId: RealtimeClientProvider.id,
154
87
  event: String(eventName || ""),
155
88
  payloadScope: payload?.scope || null,
156
89
  payloadEntityId: payload?.entityId || null
157
90
  },
158
91
  "Realtime client received socket event."
159
92
  );
160
- };
161
- socket.onAny(onAnyDebug);
162
- detach.push(() => {
163
- if (typeof socket.offAny === "function") {
164
- socket.offAny(onAnyDebug);
165
- }
166
- });
167
- }
168
- }
169
-
170
- for (const listener of listeners) {
171
- const invoke = (eventName, payload) => {
172
- const context = Object.freeze({
173
- event: eventName,
174
- payload,
175
- socket,
176
- app
177
- });
178
-
179
- if (listener.matches && listener.matches(context) !== true) {
180
- logger.debug(
181
- {
182
- listenerId: listener.listenerId,
183
- event: eventName
184
- },
185
- "Realtime client listener skipped event by matches()."
186
- );
187
- return;
188
- }
189
-
190
- logger.debug(
191
- {
192
- listenerId: listener.listenerId,
193
- event: eventName,
194
- payloadScope: payload?.scope || null,
195
- payloadEntityId: payload?.entityId || null
196
- },
197
- "Realtime client listener handling event."
198
- );
199
-
200
- Promise.resolve(listener.handle(context)).catch((error) => {
201
- logger.error(
202
- {
203
- listenerId: listener.listenerId,
204
- event: eventName,
205
- error: String(error?.message || error || "unknown error")
206
- },
207
- "Realtime client listener failed."
208
- );
209
- });
210
- };
211
-
212
- if (listener.event === "*") {
213
- if (typeof socket.onAny === "function") {
214
- const onAny = (eventName, payload) => invoke(eventName, payload);
215
- socket.onAny(onAny);
216
- detach.push(() => {
217
- if (typeof socket.offAny === "function") {
218
- socket.offAny(onAny);
219
- }
220
- });
221
- }
222
- continue;
93
+ socket.onAny(onAny);
94
+ detach.push(() => socket.offAny?.(onAny));
223
95
  }
224
-
225
- if (typeof socket.on === "function") {
226
- const onEvent = (payload) => invoke(listener.event, payload);
227
- socket.on(listener.event, onEvent);
228
- detach.push(() => {
229
- if (typeof socket.off === "function") {
230
- socket.off(listener.event, onEvent);
231
- }
232
- });
233
- }
234
- }
235
-
236
- this.socket = socket;
237
- this.detach = detach;
238
-
239
- if (!app.has("jskit.client.vue.app")) {
240
- return;
241
96
  }
242
97
 
243
- const vueApp = app.make("jskit.client.vue.app");
244
- if (!vueApp || typeof vueApp.provide !== "function") {
245
- return;
246
- }
247
- vueApp.provide("jskit.realtime.runtime.client.socket", socket);
98
+ vueApp?.provide?.("jskit.realtime.runtime.client.socket", socket);
248
99
  }
249
100
 
250
- shutdown(app) {
251
- if (Array.isArray(this.detach)) {
252
- for (const release of this.detach) {
253
- if (typeof release === "function") {
254
- try {
255
- release();
256
- } catch {}
257
- }
258
- }
101
+ function dispose() {
102
+ for (const release of detach.splice(0, detach.length)) {
103
+ try {
104
+ release();
105
+ } catch {}
259
106
  }
260
- this.detach = [];
107
+ disconnectSocketIoClient(socket);
108
+ initialized = false;
109
+ }
261
110
 
262
- if (!this.socket) {
263
- return;
264
- }
111
+ return Object.freeze({
112
+ ...REALTIME_RUNTIME_CLIENT_API,
113
+ config,
114
+ dispose,
115
+ initialize,
116
+ socket
117
+ });
118
+ }
265
119
 
266
- const runtimeApi =
267
- app && typeof app.make === "function" ? app.make("runtime.realtime.client") : REALTIME_RUNTIME_CLIENT_API;
268
- runtimeApi.disconnectSocketIoClient(this.socket);
269
- this.socket = null;
120
+ const RealtimeClientProvider = defineProvider({
121
+ id: "runtime.realtime.client",
122
+ requires: {
123
+ components: "client.components",
124
+ env: "client.env",
125
+ logger: "client.logger",
126
+ vueApp: "client.vue"
127
+ },
128
+ optional: {
129
+ mobile: "client.mobile"
130
+ },
131
+ provides: {
132
+ realtime: "client.realtime"
133
+ },
134
+ setup({ components, env, logger, mobile }) {
135
+ components.register("realtime.web.connection.indicator", RealtimeConnectionIndicator);
136
+ return {
137
+ realtime: createRealtimeClient({
138
+ config: resolveRealtimeClientConfig({ env, mobile }),
139
+ loggerInput: logger
140
+ })
141
+ };
142
+ },
143
+ boot({ vueApp }, { outputs }) {
144
+ outputs.realtime.initialize(vueApp);
145
+ },
146
+ shutdown(_dependencies, { outputs }) {
147
+ outputs.realtime.dispose();
270
148
  }
271
- }
149
+ });
272
150
 
273
- export { RealtimeClientProvider };
151
+ export { RealtimeClientProvider, resolveRealtimeClientConfig };