sonamu 0.9.4 → 0.9.6

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.
Files changed (171) hide show
  1. package/dist/ai/providers/rtzr/utils.js +2 -2
  2. package/dist/api/config.d.ts +13 -2
  3. package/dist/api/config.d.ts.map +1 -1
  4. package/dist/api/config.js +1 -1
  5. package/dist/api/context.d.ts +17 -7
  6. package/dist/api/context.d.ts.map +1 -1
  7. package/dist/api/context.js +1 -1
  8. package/dist/api/decorators.d.ts +18 -0
  9. package/dist/api/decorators.d.ts.map +1 -1
  10. package/dist/api/decorators.js +54 -3
  11. package/dist/api/index.js +8 -3
  12. package/dist/api/sonamu.d.ts +24 -9
  13. package/dist/api/sonamu.d.ts.map +1 -1
  14. package/dist/api/sonamu.js +365 -79
  15. package/dist/api/websocket-helpers.d.ts +24 -0
  16. package/dist/api/websocket-helpers.d.ts.map +1 -0
  17. package/dist/api/websocket-helpers.js +77 -0
  18. package/dist/bin/cli.js +12 -4
  19. package/dist/database/upsert-builder.js +4 -4
  20. package/dist/dict/sonamu-dictionary.js +6 -6
  21. package/dist/entity/entity-manager.js +1 -1
  22. package/dist/entity/entity.js +3 -3
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +16 -4
  26. package/dist/migration/code-generation.d.ts.map +1 -1
  27. package/dist/migration/code-generation.js +8 -9
  28. package/dist/stream/index.d.ts +6 -0
  29. package/dist/stream/index.d.ts.map +1 -1
  30. package/dist/stream/index.js +13 -2
  31. package/dist/stream/ws-audience-resolver.d.ts +15 -0
  32. package/dist/stream/ws-audience-resolver.d.ts.map +1 -0
  33. package/dist/stream/ws-audience-resolver.js +31 -0
  34. package/dist/stream/ws-audience.d.ts +28 -0
  35. package/dist/stream/ws-audience.d.ts.map +1 -0
  36. package/dist/stream/ws-audience.js +46 -0
  37. package/dist/stream/ws-cluster-bus.d.ts +23 -0
  38. package/dist/stream/ws-cluster-bus.d.ts.map +1 -0
  39. package/dist/stream/ws-cluster-bus.js +18 -0
  40. package/dist/stream/ws-core.d.ts +15 -0
  41. package/dist/stream/ws-core.d.ts.map +1 -0
  42. package/dist/stream/ws-core.js +1 -0
  43. package/dist/stream/ws-delivery.d.ts +24 -0
  44. package/dist/stream/ws-delivery.d.ts.map +1 -0
  45. package/dist/stream/ws-delivery.js +103 -0
  46. package/dist/stream/ws-local-connection-store.d.ts +10 -0
  47. package/dist/stream/ws-local-connection-store.d.ts.map +1 -0
  48. package/dist/stream/ws-local-connection-store.js +44 -0
  49. package/dist/stream/ws-presence-store.d.ts +61 -0
  50. package/dist/stream/ws-presence-store.d.ts.map +1 -0
  51. package/dist/stream/ws-presence-store.js +236 -0
  52. package/dist/stream/ws-registry.d.ts +42 -0
  53. package/dist/stream/ws-registry.d.ts.map +1 -0
  54. package/dist/stream/ws-registry.js +108 -0
  55. package/dist/stream/ws.d.ts +52 -0
  56. package/dist/stream/ws.d.ts.map +1 -0
  57. package/dist/stream/ws.js +397 -0
  58. package/dist/syncer/api-parser.d.ts.map +1 -1
  59. package/dist/syncer/api-parser.js +72 -2
  60. package/dist/syncer/checksum.d.ts.map +1 -1
  61. package/dist/syncer/checksum.js +13 -12
  62. package/dist/syncer/code-generator.d.ts.map +1 -1
  63. package/dist/syncer/code-generator.js +7 -4
  64. package/dist/syncer/event-batcher.d.ts +27 -0
  65. package/dist/syncer/event-batcher.d.ts.map +1 -0
  66. package/dist/syncer/event-batcher.js +69 -0
  67. package/dist/syncer/file-patterns.d.ts +48 -26
  68. package/dist/syncer/file-patterns.d.ts.map +1 -1
  69. package/dist/syncer/file-patterns.js +71 -23
  70. package/dist/syncer/file-tracking.d.ts +13 -0
  71. package/dist/syncer/file-tracking.d.ts.map +1 -0
  72. package/dist/syncer/file-tracking.js +33 -0
  73. package/dist/syncer/index.js +2 -2
  74. package/dist/syncer/module-loader.d.ts +2 -11
  75. package/dist/syncer/module-loader.d.ts.map +1 -1
  76. package/dist/syncer/module-loader.js +3 -3
  77. package/dist/syncer/syncer-actions.d.ts +39 -6
  78. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  79. package/dist/syncer/syncer-actions.js +125 -10
  80. package/dist/syncer/syncer.d.ts +33 -19
  81. package/dist/syncer/syncer.d.ts.map +1 -1
  82. package/dist/syncer/syncer.js +168 -168
  83. package/dist/syncer/watcher.d.ts +8 -0
  84. package/dist/syncer/watcher.d.ts.map +1 -0
  85. package/dist/syncer/watcher.js +105 -0
  86. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  87. package/dist/tasks/workflow-manager.js +2 -1
  88. package/dist/template/implementations/services.template.d.ts.map +1 -1
  89. package/dist/template/implementations/services.template.js +36 -1
  90. package/dist/testing/bootstrap.d.ts.map +1 -1
  91. package/dist/testing/bootstrap.js +8 -1
  92. package/dist/testing/data-explorer.d.ts.map +1 -1
  93. package/dist/testing/data-explorer.js +5 -3
  94. package/dist/testing/fixture-manager.js +1 -1
  95. package/dist/types/types.d.ts +2 -1
  96. package/dist/types/types.d.ts.map +1 -1
  97. package/dist/types/types.js +2 -2
  98. package/dist/ui/api.d.ts.map +1 -1
  99. package/dist/ui/api.js +4 -3
  100. package/dist/ui/cdd-service.js +1 -1
  101. package/dist/ui-web/assets/{index-C5KUjXm0.js → index-BmThfg-s.js} +39 -39
  102. package/dist/ui-web/assets/index-D4rYm-Xz.css +1 -0
  103. package/dist/ui-web/index.html +2 -2
  104. package/dist/utils/async-utils.d.ts +27 -3
  105. package/dist/utils/async-utils.d.ts.map +1 -1
  106. package/dist/utils/async-utils.js +56 -6
  107. package/dist/utils/formatter.d.ts +7 -1
  108. package/dist/utils/formatter.d.ts.map +1 -1
  109. package/dist/utils/formatter.js +95 -60
  110. package/dist/utils/fs-utils.d.ts +2 -0
  111. package/dist/utils/fs-utils.d.ts.map +1 -1
  112. package/dist/utils/fs-utils.js +10 -2
  113. package/dist/utils/process-utils.d.ts +6 -0
  114. package/dist/utils/process-utils.d.ts.map +1 -1
  115. package/dist/utils/process-utils.js +16 -3
  116. package/dist/utils/utils.d.ts +1 -0
  117. package/dist/utils/utils.d.ts.map +1 -1
  118. package/dist/utils/utils.js +2 -2
  119. package/package.json +7 -5
  120. package/src/ai/providers/rtzr/utils.ts +1 -1
  121. package/src/api/__tests__/sonamu.websocket.test.ts +64 -0
  122. package/src/api/__tests__/websocket-context.types.test.ts +58 -0
  123. package/src/api/config.ts +28 -2
  124. package/src/api/context.ts +21 -7
  125. package/src/api/decorators.ts +101 -1
  126. package/src/api/sonamu.ts +529 -127
  127. package/src/api/websocket-helpers.ts +122 -0
  128. package/src/bin/cli.ts +10 -2
  129. package/src/database/upsert-builder.ts +3 -3
  130. package/src/dict/sonamu-dictionary.ts +3 -3
  131. package/src/entity/entity.ts +1 -1
  132. package/src/index.ts +6 -0
  133. package/src/migration/code-generation.ts +6 -11
  134. package/src/shared/app.shared.ts.txt +312 -4
  135. package/src/shared/web.shared.ts.txt +340 -4
  136. package/src/stream/__tests__/ws-contracts.test.ts +381 -0
  137. package/src/stream/__tests__/ws.test.ts +449 -0
  138. package/src/stream/index.ts +6 -0
  139. package/src/stream/ws-audience-resolver.ts +35 -0
  140. package/src/stream/ws-audience.ts +62 -0
  141. package/src/stream/ws-cluster-bus.ts +32 -0
  142. package/src/stream/ws-core.ts +16 -0
  143. package/src/stream/ws-delivery.ts +138 -0
  144. package/src/stream/ws-local-connection-store.ts +44 -0
  145. package/src/stream/ws-presence-store.ts +326 -0
  146. package/src/stream/ws-registry.ts +138 -0
  147. package/src/stream/ws.ts +591 -0
  148. package/src/syncer/__tests__/api-parser.websocket-type-ref.test.ts +78 -0
  149. package/src/syncer/api-parser.ts +112 -1
  150. package/src/syncer/checksum.ts +23 -29
  151. package/src/syncer/code-generator.ts +4 -1
  152. package/src/syncer/event-batcher.ts +72 -0
  153. package/src/syncer/file-patterns.ts +98 -30
  154. package/src/syncer/file-tracking.ts +27 -0
  155. package/src/syncer/module-loader.ts +5 -12
  156. package/src/syncer/syncer-actions.ts +179 -17
  157. package/src/syncer/syncer.ts +250 -287
  158. package/src/syncer/watcher.ts +128 -0
  159. package/src/tasks/workflow-manager.ts +1 -0
  160. package/src/template/__tests__/services.template.websocket.test.ts +79 -0
  161. package/src/template/implementations/services.template.ts +69 -0
  162. package/src/testing/bootstrap.ts +8 -1
  163. package/src/testing/data-explorer.ts +3 -2
  164. package/src/types/types.ts +20 -2
  165. package/src/ui/api.ts +10 -1
  166. package/src/utils/async-utils.ts +71 -4
  167. package/src/utils/formatter.ts +114 -75
  168. package/src/utils/fs-utils.ts +9 -0
  169. package/src/utils/process-utils.ts +17 -0
  170. package/src/utils/utils.ts +1 -1
  171. package/dist/ui-web/assets/index-Dr8pRJC_.css +0 -1
@@ -0,0 +1,381 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { WebSocketAudience } from "../ws-audience";
4
+ import { WebSocketAudienceResolver } from "../ws-audience-resolver";
5
+ import {
6
+ NoopWebSocketClusterBus,
7
+ type WebSocketClusterBus,
8
+ type WebSocketClusterEnvelope,
9
+ type WebSocketClusterEnvelopeHandler,
10
+ } from "../ws-cluster-bus";
11
+ import { type ManagedWebSocketConnection } from "../ws-core";
12
+ import { WebSocketDeliveryEngine } from "../ws-delivery";
13
+ import { WebSocketLocalConnectionStore } from "../ws-local-connection-store";
14
+ import { InMemoryWebSocketPresenceStore } from "../ws-presence-store";
15
+
16
+ async function waitForAsyncQueue(rounds: number = 3): Promise<void> {
17
+ for (let index = 0; index < rounds; index += 1) {
18
+ await new Promise<void>((resolve) => {
19
+ setImmediate(resolve);
20
+ });
21
+ }
22
+ }
23
+
24
+ class ContractClusterBus implements WebSocketClusterBus {
25
+ readonly published: WebSocketClusterEnvelope[] = [];
26
+ private readonly handlers = new Set<WebSocketClusterEnvelopeHandler>();
27
+
28
+ publish(envelope: WebSocketClusterEnvelope): void {
29
+ this.published.push(envelope);
30
+ }
31
+
32
+ subscribe(handler: WebSocketClusterEnvelopeHandler): () => void {
33
+ this.handlers.add(handler);
34
+ return () => {
35
+ this.handlers.delete(handler);
36
+ };
37
+ }
38
+
39
+ emit(envelope: WebSocketClusterEnvelope): void {
40
+ for (const handler of this.handlers) {
41
+ handler(envelope);
42
+ }
43
+ }
44
+
45
+ shutdown(): void {
46
+ this.handlers.clear();
47
+ }
48
+ }
49
+
50
+ class ContractConnection implements ManagedWebSocketConnection {
51
+ readonly sent: Array<{ event: string; data: unknown }> = [];
52
+ closed = false;
53
+
54
+ constructor(
55
+ readonly id: string,
56
+ readonly namespace: string,
57
+ ) {}
58
+
59
+ publishUntyped(event: string, data: unknown): void {
60
+ this.sent.push({ event, data });
61
+ }
62
+
63
+ close(): void {
64
+ this.closed = true;
65
+ }
66
+ }
67
+
68
+ describe("WebSocket contract tests", () => {
69
+ describe("PresenceStore", () => {
70
+ it("keeps inactive sessions out of counts and audience queries until activation", () => {
71
+ const store = new InMemoryWebSocketPresenceStore();
72
+ store.register({
73
+ sessionId: "s1",
74
+ nodeId: "node-a",
75
+ namespace: "chat",
76
+ active: false,
77
+ });
78
+
79
+ expect(store.getConnectionCount("chat")).toBe(0);
80
+ expect(store.queryAudience(WebSocketAudience.all("chat"))).toHaveLength(0);
81
+
82
+ store.activate("s1");
83
+
84
+ expect(store.getConnectionCount("chat")).toBe(1);
85
+ expect(
86
+ store.queryAudience(WebSocketAudience.all("chat")).map((meta) => meta.sessionId),
87
+ ).toEqual(["s1"]);
88
+ });
89
+
90
+ it("isolates room and user bindings by namespace", () => {
91
+ const store = new InMemoryWebSocketPresenceStore();
92
+ store.register({
93
+ sessionId: "chat-1",
94
+ nodeId: "node-a",
95
+ namespace: "chat",
96
+ });
97
+ store.register({
98
+ sessionId: "admin-1",
99
+ nodeId: "node-a",
100
+ namespace: "admin",
101
+ });
102
+
103
+ store.join("chat-1", "room-1");
104
+ store.join("admin-1", "room-1");
105
+ store.setUserId("chat-1", "user-1");
106
+ store.setUserId("admin-1", "user-1");
107
+
108
+ expect(
109
+ store.queryAudience(WebSocketAudience.room("room-1", "chat")).map((meta) => meta.sessionId),
110
+ ).toEqual(["chat-1"]);
111
+ expect(
112
+ store
113
+ .queryAudience(WebSocketAudience.user("user-1", "admin"))
114
+ .map((meta) => meta.sessionId),
115
+ ).toEqual(["admin-1"]);
116
+ });
117
+
118
+ it("cleans room and user bindings when a session is unregistered", () => {
119
+ const store = new InMemoryWebSocketPresenceStore();
120
+ store.register({
121
+ sessionId: "s1",
122
+ nodeId: "node-a",
123
+ namespace: "chat",
124
+ });
125
+ store.join("s1", "room-1");
126
+ store.setUserId("s1", "user-1");
127
+
128
+ expect(store.unregister("s1")?.sessionId).toBe("s1");
129
+
130
+ expect(store.getConnection("s1")).toBeUndefined();
131
+ expect(store.queryAudience(WebSocketAudience.room("room-1", "chat"))).toHaveLength(0);
132
+ expect(store.queryAudience(WebSocketAudience.user("user-1", "chat"))).toHaveLength(0);
133
+ expect(store.getStats()).toMatchObject({
134
+ totalConnections: 0,
135
+ totalRooms: 0,
136
+ });
137
+ });
138
+
139
+ it("dedupes union audience results", () => {
140
+ const store = new InMemoryWebSocketPresenceStore();
141
+ store.register({
142
+ sessionId: "s1",
143
+ nodeId: "node-a",
144
+ namespace: "chat",
145
+ });
146
+ store.join("s1", "room-1");
147
+ store.setUserId("s1", "user-1");
148
+
149
+ const metas = store.queryAudience(
150
+ WebSocketAudience.union(
151
+ WebSocketAudience.room("room-1", "chat"),
152
+ WebSocketAudience.user("user-1", "chat"),
153
+ ),
154
+ );
155
+
156
+ expect(metas.map((meta) => meta.sessionId)).toEqual(["s1"]);
157
+ });
158
+ });
159
+
160
+ describe("AudienceResolver", () => {
161
+ it("splits local session ids from unique remote node ids", () => {
162
+ const store = new InMemoryWebSocketPresenceStore();
163
+ store.register({
164
+ sessionId: "local-1",
165
+ nodeId: "node-a",
166
+ namespace: "chat",
167
+ });
168
+ store.register({
169
+ sessionId: "remote-1",
170
+ nodeId: "node-b",
171
+ namespace: "chat",
172
+ });
173
+ store.register({
174
+ sessionId: "remote-2",
175
+ nodeId: "node-b",
176
+ namespace: "chat",
177
+ });
178
+ store.join("local-1", "room-1");
179
+ store.join("remote-1", "room-1");
180
+ store.join("remote-2", "room-1");
181
+ const resolver = new WebSocketAudienceResolver({
182
+ nodeId: "node-a",
183
+ presenceStore: store,
184
+ });
185
+
186
+ expect(resolver.resolve(WebSocketAudience.room("room-1", "chat"))).toEqual({
187
+ localSessionIds: ["local-1"],
188
+ remoteNodeIds: ["node-b"],
189
+ });
190
+ });
191
+ });
192
+
193
+ describe("ClusterBus", () => {
194
+ it("allows subscribers to unsubscribe without affecting later publishes", () => {
195
+ const bus = new ContractClusterBus();
196
+ const received: string[] = [];
197
+ const unsubscribe = bus.subscribe((envelope) => {
198
+ received.push(envelope.id);
199
+ });
200
+
201
+ bus.emit(createEnvelope("e1"));
202
+ unsubscribe();
203
+ bus.emit(createEnvelope("e2"));
204
+
205
+ expect(received).toEqual(["e1"]);
206
+ });
207
+
208
+ it("keeps NoopClusterBus publish, subscribe, and shutdown safe", async () => {
209
+ const bus = new NoopWebSocketClusterBus();
210
+ const unsubscribe = bus.subscribe(() => {
211
+ throw new Error("noop bus must not retain handlers");
212
+ });
213
+
214
+ await Promise.resolve(bus.publish(createEnvelope("noop")));
215
+ expect(unsubscribe()).toBeUndefined();
216
+ await Promise.resolve(bus.shutdown());
217
+ });
218
+ });
219
+
220
+ describe("DeliveryEngine", () => {
221
+ it("queues local delivery and reports queued local target count", async () => {
222
+ const presenceStore = new InMemoryWebSocketPresenceStore();
223
+ const localConnections = new WebSocketLocalConnectionStore();
224
+ const clusterBus = new ContractClusterBus();
225
+ const connection = new ContractConnection("s1", "chat");
226
+ localConnections.register(connection);
227
+ presenceStore.register({
228
+ sessionId: "s1",
229
+ nodeId: "node-a",
230
+ namespace: "chat",
231
+ });
232
+ const engine = createDeliveryEngine({
233
+ nodeId: "node-a",
234
+ presenceStore,
235
+ localConnections,
236
+ clusterBus,
237
+ });
238
+
239
+ engine.publishToAudience(WebSocketAudience.all("chat"), "onReady", {
240
+ ok: true,
241
+ });
242
+
243
+ expect(connection.sent).toHaveLength(0);
244
+
245
+ await waitForAsyncQueue();
246
+
247
+ expect(connection.sent).toEqual([
248
+ {
249
+ event: "onReady",
250
+ data: {
251
+ ok: true,
252
+ },
253
+ },
254
+ ]);
255
+ });
256
+
257
+ it("publishes one remote envelope per remote-node routing plan", async () => {
258
+ const presenceStore = new InMemoryWebSocketPresenceStore();
259
+ const localConnections = new WebSocketLocalConnectionStore();
260
+ const clusterBus = new ContractClusterBus();
261
+ presenceStore.register({
262
+ sessionId: "remote-1",
263
+ nodeId: "node-b",
264
+ namespace: "chat",
265
+ });
266
+ presenceStore.join("remote-1", "room-1");
267
+ const engine = createDeliveryEngine({
268
+ nodeId: "node-a",
269
+ presenceStore,
270
+ localConnections,
271
+ clusterBus,
272
+ });
273
+
274
+ engine.publishToAudience(WebSocketAudience.room("room-1", "chat"), "evt", {
275
+ ok: true,
276
+ });
277
+
278
+ await waitForAsyncQueue();
279
+
280
+ expect(clusterBus.published).toHaveLength(1);
281
+ expect(clusterBus.published[0]).toMatchObject({
282
+ sourceNodeId: "node-a",
283
+ targetNodeIds: ["node-b"],
284
+ event: "evt",
285
+ });
286
+ });
287
+
288
+ it("ignores cluster envelopes from itself or targeted to another node", async () => {
289
+ const presenceStore = new InMemoryWebSocketPresenceStore();
290
+ const localConnections = new WebSocketLocalConnectionStore();
291
+ const clusterBus = new ContractClusterBus();
292
+ const connection = new ContractConnection("s1", "chat");
293
+ localConnections.register(connection);
294
+ presenceStore.register({
295
+ sessionId: "s1",
296
+ nodeId: "node-a",
297
+ namespace: "chat",
298
+ });
299
+ createDeliveryEngine({
300
+ nodeId: "node-a",
301
+ presenceStore,
302
+ localConnections,
303
+ clusterBus,
304
+ });
305
+
306
+ clusterBus.emit(createEnvelope("self", "node-a", ["node-a"]));
307
+ clusterBus.emit(createEnvelope("other-target", "node-b", ["node-c"]));
308
+
309
+ await waitForAsyncQueue();
310
+
311
+ expect(connection.sent).toHaveLength(0);
312
+ });
313
+
314
+ it("delivers accepted cluster envelopes only to local targets", async () => {
315
+ const presenceStore = new InMemoryWebSocketPresenceStore();
316
+ const localConnections = new WebSocketLocalConnectionStore();
317
+ const clusterBus = new ContractClusterBus();
318
+ const connection = new ContractConnection("s1", "chat");
319
+ localConnections.register(connection);
320
+ presenceStore.register({
321
+ sessionId: "s1",
322
+ nodeId: "node-a",
323
+ namespace: "chat",
324
+ });
325
+ createDeliveryEngine({
326
+ nodeId: "node-a",
327
+ presenceStore,
328
+ localConnections,
329
+ clusterBus,
330
+ });
331
+
332
+ clusterBus.emit(createEnvelope("remote", "node-b", ["node-a"]));
333
+
334
+ await waitForAsyncQueue();
335
+
336
+ expect(connection.sent).toEqual([
337
+ {
338
+ event: "evt",
339
+ data: {
340
+ ok: true,
341
+ },
342
+ },
343
+ ]);
344
+ });
345
+ });
346
+ });
347
+
348
+ function createDeliveryEngine(input: {
349
+ nodeId: string;
350
+ presenceStore: InMemoryWebSocketPresenceStore;
351
+ localConnections: WebSocketLocalConnectionStore;
352
+ clusterBus: WebSocketClusterBus;
353
+ }): WebSocketDeliveryEngine {
354
+ return new WebSocketDeliveryEngine({
355
+ nodeId: input.nodeId,
356
+ localConnections: input.localConnections,
357
+ audienceResolver: new WebSocketAudienceResolver({
358
+ nodeId: input.nodeId,
359
+ presenceStore: input.presenceStore,
360
+ }),
361
+ clusterBus: input.clusterBus,
362
+ });
363
+ }
364
+
365
+ function createEnvelope(
366
+ id: string,
367
+ sourceNodeId = "node-b",
368
+ targetNodeIds: string[] | undefined = ["node-a"],
369
+ ): WebSocketClusterEnvelope {
370
+ return {
371
+ id,
372
+ sourceNodeId,
373
+ targetNodeIds,
374
+ audience: WebSocketAudience.all("chat"),
375
+ event: "evt",
376
+ data: {
377
+ ok: true,
378
+ },
379
+ emittedAt: Date.now(),
380
+ };
381
+ }