@objectstack/service-realtime 4.0.3 → 4.0.5

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/README.md ADDED
@@ -0,0 +1,438 @@
1
+ # @objectstack/service-realtime
2
+
3
+ Realtime Service for ObjectStack — implements `IRealtimeService` with WebSocket and in-memory pub/sub.
4
+
5
+ ## Features
6
+
7
+ - **WebSocket Support**: Real-time bidirectional communication
8
+ - **Pub/Sub Pattern**: Subscribe to channels and receive updates
9
+ - **Room-Based Architecture**: Organize connections into rooms
10
+ - **Presence Tracking**: Track online users and their status
11
+ - **Message Broadcasting**: Send messages to all connections or specific rooms
12
+ - **Event Streaming**: Stream database changes and system events
13
+ - **Auto-Reconnection**: Client auto-reconnects on connection loss
14
+ - **Type-Safe**: Full TypeScript support for events and messages
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @objectstack/service-realtime
20
+ ```
21
+
22
+ ## Basic Usage
23
+
24
+ ```typescript
25
+ import { defineStack } from '@objectstack/spec';
26
+ import { ServiceRealtime } from '@objectstack/service-realtime';
27
+
28
+ const stack = defineStack({
29
+ services: [
30
+ ServiceRealtime.configure({
31
+ port: 3001,
32
+ path: '/ws',
33
+ }),
34
+ ],
35
+ });
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ ```typescript
41
+ interface RealtimeServiceConfig {
42
+ /** WebSocket server port (default: 3001) */
43
+ port?: number;
44
+
45
+ /** WebSocket path (default: '/ws') */
46
+ path?: string;
47
+
48
+ /** Enable CORS (default: true) */
49
+ cors?: boolean;
50
+
51
+ /** Maximum connections per user (default: 10) */
52
+ maxConnectionsPerUser?: number;
53
+
54
+ /** Ping interval in ms (default: 30000) */
55
+ pingInterval?: number;
56
+ }
57
+ ```
58
+
59
+ ## Service API (Server-Side)
60
+
61
+ ```typescript
62
+ // Get realtime service
63
+ const realtime = kernel.getService<IRealtimeService>('realtime');
64
+ ```
65
+
66
+ ### Broadcasting
67
+
68
+ ```typescript
69
+ // Broadcast to all connected clients
70
+ await realtime.broadcast({
71
+ event: 'notification',
72
+ data: { message: 'System update in 5 minutes' },
73
+ });
74
+
75
+ // Broadcast to specific room
76
+ await realtime.broadcastToRoom('opportunity:123', {
77
+ event: 'record_updated',
78
+ data: { recordId: '123', field: 'stage', value: 'closed_won' },
79
+ });
80
+
81
+ // Broadcast to specific user
82
+ await realtime.broadcastToUser('user:456', {
83
+ event: 'mention',
84
+ data: { commentId: 'comment:789', mentionedBy: 'user:123' },
85
+ });
86
+ ```
87
+
88
+ ### Channel Management
89
+
90
+ ```typescript
91
+ // Join a channel (room)
92
+ await realtime.join(connectionId, 'opportunity:123');
93
+
94
+ // Leave a channel
95
+ await realtime.leave(connectionId, 'opportunity:123');
96
+
97
+ // Get all connections in a channel
98
+ const connections = await realtime.getChannelConnections('opportunity:123');
99
+
100
+ // Get all channels for a connection
101
+ const channels = await realtime.getConnectionChannels(connectionId);
102
+ ```
103
+
104
+ ### Presence
105
+
106
+ ```typescript
107
+ // Set user presence
108
+ await realtime.setPresence('user:456', {
109
+ status: 'online',
110
+ currentPage: '/opportunity/123',
111
+ lastActive: new Date(),
112
+ });
113
+
114
+ // Get user presence
115
+ const presence = await realtime.getPresence('user:456');
116
+
117
+ // Get all online users
118
+ const onlineUsers = await realtime.getOnlineUsers();
119
+
120
+ // Get users in a specific channel
121
+ const channelUsers = await realtime.getChannelPresence('opportunity:123');
122
+ ```
123
+
124
+ ## Client-Side Usage
125
+
126
+ ### React Hook
127
+
128
+ ```typescript
129
+ import { useRealtime } from '@objectstack/client-react';
130
+
131
+ function OpportunityDetails({ id }: { id: string }) {
132
+ const { subscribe, send, isConnected } = useRealtime();
133
+
134
+ useEffect(() => {
135
+ // Subscribe to record updates
136
+ const unsubscribe = subscribe(`opportunity:${id}`, (event) => {
137
+ if (event.type === 'record_updated') {
138
+ console.log('Record updated:', event.data);
139
+ // Update UI
140
+ }
141
+ });
142
+
143
+ return unsubscribe;
144
+ }, [id]);
145
+
146
+ return (
147
+ <div>
148
+ {isConnected ? '🟢 Connected' : '🔴 Disconnected'}
149
+ </div>
150
+ );
151
+ }
152
+ ```
153
+
154
+ ### JavaScript Client
155
+
156
+ ```typescript
157
+ import { RealtimeClient } from '@objectstack/client';
158
+
159
+ const client = new RealtimeClient({
160
+ url: 'ws://localhost:3001/ws',
161
+ auth: {
162
+ token: 'your-auth-token',
163
+ },
164
+ });
165
+
166
+ // Connect
167
+ await client.connect();
168
+
169
+ // Subscribe to a channel
170
+ client.subscribe('opportunity:123', (event) => {
171
+ console.log('Received event:', event);
172
+ });
173
+
174
+ // Send a message
175
+ client.send('typing', {
176
+ recordId: '123',
177
+ userId: 'user:456',
178
+ isTyping: true,
179
+ });
180
+
181
+ // Disconnect
182
+ await client.disconnect();
183
+ ```
184
+
185
+ ## Advanced Features
186
+
187
+ ### Event Streaming
188
+
189
+ Stream database changes in real-time:
190
+
191
+ ```typescript
192
+ // Server-side: Stream record changes
193
+ realtime.streamRecordChanges('opportunity', {
194
+ onInsert: async (record) => {
195
+ await realtime.broadcast({
196
+ event: 'record_created',
197
+ data: { object: 'opportunity', record },
198
+ });
199
+ },
200
+ onUpdate: async (record, changes) => {
201
+ await realtime.broadcastToRoom(`opportunity:${record.id}`, {
202
+ event: 'record_updated',
203
+ data: { recordId: record.id, changes },
204
+ });
205
+ },
206
+ onDelete: async (recordId) => {
207
+ await realtime.broadcast({
208
+ event: 'record_deleted',
209
+ data: { object: 'opportunity', recordId },
210
+ });
211
+ },
212
+ });
213
+ ```
214
+
215
+ ### Private Channels
216
+
217
+ ```typescript
218
+ // Server-side: Authorize private channel access
219
+ realtime.authorizeChannel = async (userId, channel) => {
220
+ if (channel.startsWith('user:')) {
221
+ // Only allow users to join their own private channel
222
+ return channel === `user:${userId}`;
223
+ }
224
+
225
+ if (channel.startsWith('opportunity:')) {
226
+ // Check if user has access to the opportunity
227
+ const opportunityId = channel.split(':')[1];
228
+ return await hasAccess(userId, 'opportunity', opportunityId);
229
+ }
230
+
231
+ return false;
232
+ };
233
+ ```
234
+
235
+ ### Typing Indicators
236
+
237
+ ```typescript
238
+ // Client sends typing event
239
+ client.send('typing', {
240
+ recordId: '123',
241
+ userId: 'user:456',
242
+ isTyping: true,
243
+ });
244
+
245
+ // Server broadcasts to room
246
+ realtime.on('typing', async (connectionId, data) => {
247
+ await realtime.broadcastToRoom(`opportunity:${data.recordId}`, {
248
+ event: 'user_typing',
249
+ data: { userId: data.userId, isTyping: data.isTyping },
250
+ }, { exclude: [connectionId] }); // Don't send back to sender
251
+ });
252
+
253
+ // Other clients receive typing notification
254
+ client.subscribe('opportunity:123', (event) => {
255
+ if (event.type === 'user_typing') {
256
+ showTypingIndicator(event.data.userId, event.data.isTyping);
257
+ }
258
+ });
259
+ ```
260
+
261
+ ### Live Cursor Tracking
262
+
263
+ ```typescript
264
+ // Client sends cursor position
265
+ client.send('cursor', {
266
+ recordId: '123',
267
+ x: 450,
268
+ y: 200,
269
+ });
270
+
271
+ // Server broadcasts to room
272
+ realtime.on('cursor', async (connectionId, data) => {
273
+ const user = await getConnectionUser(connectionId);
274
+
275
+ await realtime.broadcastToRoom(`opportunity:${data.recordId}`, {
276
+ event: 'cursor_moved',
277
+ data: {
278
+ userId: user.id,
279
+ userName: user.name,
280
+ x: data.x,
281
+ y: data.y,
282
+ },
283
+ }, { exclude: [connectionId] });
284
+ });
285
+ ```
286
+
287
+ ### Collaborative Editing
288
+
289
+ ```typescript
290
+ // Operational Transform (OT) for collaborative editing
291
+ client.send('edit', {
292
+ documentId: '123',
293
+ operation: {
294
+ type: 'insert',
295
+ position: 42,
296
+ text: 'Hello',
297
+ },
298
+ });
299
+
300
+ realtime.on('edit', async (connectionId, data) => {
301
+ // Apply operation transform
302
+ const transformedOp = await applyOT(data.operation);
303
+
304
+ // Broadcast to all editors
305
+ await realtime.broadcastToRoom(`document:${data.documentId}`, {
306
+ event: 'operation',
307
+ data: transformedOp,
308
+ }, { exclude: [connectionId] });
309
+ });
310
+ ```
311
+
312
+ ## Integration with ObjectStack Features
313
+
314
+ ### Feed Updates
315
+
316
+ ```typescript
317
+ // When a comment is added
318
+ feed.on('comment_added', async (comment) => {
319
+ await realtime.broadcastToRoom(`${comment.object}:${comment.recordId}`, {
320
+ event: 'feed_update',
321
+ data: { type: 'comment', comment },
322
+ });
323
+ });
324
+ ```
325
+
326
+ ### Workflow Status
327
+
328
+ ```typescript
329
+ // When a flow step completes
330
+ automation.on('step_completed', async (execution) => {
331
+ await realtime.broadcastToUser(execution.userId, {
332
+ event: 'flow_progress',
333
+ data: {
334
+ flowId: execution.flowId,
335
+ step: execution.currentStep,
336
+ progress: execution.progress,
337
+ },
338
+ });
339
+ });
340
+ ```
341
+
342
+ ### Analytics Dashboard
343
+
344
+ ```typescript
345
+ // Stream real-time metrics
346
+ setInterval(async () => {
347
+ const metrics = await analytics.getCurrentMetrics();
348
+
349
+ await realtime.broadcastToRoom('dashboard:sales', {
350
+ event: 'metrics_update',
351
+ data: metrics,
352
+ });
353
+ }, 5000); // Every 5 seconds
354
+ ```
355
+
356
+ ## Connection Events
357
+
358
+ ```typescript
359
+ // Server-side event handlers
360
+ realtime.on('connection', async (connectionId, userId) => {
361
+ console.log(`User ${userId} connected (${connectionId})`);
362
+
363
+ // Set initial presence
364
+ await realtime.setPresence(userId, { status: 'online' });
365
+ });
366
+
367
+ realtime.on('disconnection', async (connectionId, userId) => {
368
+ console.log(`User ${userId} disconnected`);
369
+
370
+ // Update presence
371
+ await realtime.setPresence(userId, {
372
+ status: 'offline',
373
+ lastSeen: new Date(),
374
+ });
375
+ });
376
+
377
+ realtime.on('error', async (connectionId, error) => {
378
+ console.error(`Connection error:`, error);
379
+ });
380
+ ```
381
+
382
+ ## Best Practices
383
+
384
+ 1. **Channel Organization**: Use namespaced channels (e.g., `object:recordId`)
385
+ 2. **Authorization**: Always verify channel access before joining
386
+ 3. **Message Size**: Keep messages small (< 10KB)
387
+ 4. **Rate Limiting**: Limit message frequency per connection
388
+ 5. **Cleanup**: Remove disconnected users from channels
389
+ 6. **Heartbeat**: Implement ping/pong for connection health
390
+ 7. **Compression**: Enable WebSocket compression for large messages
391
+
392
+ ## Performance Considerations
393
+
394
+ - **Scaling**: Use Redis adapter for multi-server deployments
395
+ - **Connection Pooling**: Limit concurrent connections per user
396
+ - **Channel Limits**: Limit channels per connection
397
+ - **Message Batching**: Batch frequent updates to reduce traffic
398
+ - **Binary Protocol**: Use binary for large data transfers
399
+
400
+ ## REST API Endpoints
401
+
402
+ ```
403
+ POST /api/v1/realtime/broadcast # Broadcast to all
404
+ POST /api/v1/realtime/broadcast/room/:room # Broadcast to room
405
+ POST /api/v1/realtime/broadcast/user/:userId # Broadcast to user
406
+ GET /api/v1/realtime/presence # Get online users
407
+ GET /api/v1/realtime/presence/:userId # Get user presence
408
+ GET /api/v1/realtime/channels/:channel # Get channel connections
409
+ ```
410
+
411
+ ## Contract Implementation
412
+
413
+ Implements `IRealtimeService` from `@objectstack/spec/contracts`:
414
+
415
+ ```typescript
416
+ interface IRealtimeService {
417
+ broadcast(message: Message): Promise<void>;
418
+ broadcastToRoom(room: string, message: Message): Promise<void>;
419
+ broadcastToUser(userId: string, message: Message): Promise<void>;
420
+ join(connectionId: string, channel: string): Promise<void>;
421
+ leave(connectionId: string, channel: string): Promise<void>;
422
+ setPresence(userId: string, presence: PresenceData): Promise<void>;
423
+ getPresence(userId: string): Promise<PresenceData | null>;
424
+ getOnlineUsers(): Promise<string[]>;
425
+ on(event: string, handler: EventHandler): void;
426
+ }
427
+ ```
428
+
429
+ ## License
430
+
431
+ Apache-2.0
432
+
433
+ ## See Also
434
+
435
+ - [WebSocket API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
436
+ - [@objectstack/client](../../client/)
437
+ - [@objectstack/client-react](../../client-react/)
438
+ - [Realtime Features Guide](/content/docs/guides/realtime/)
package/dist/index.cjs CHANGED
@@ -21,11 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  InMemoryRealtimeAdapter: () => InMemoryRealtimeAdapter,
24
- RealtimeServicePlugin: () => RealtimeServicePlugin,
25
- SysPresence: () => SysPresence
24
+ RealtimeServicePlugin: () => RealtimeServicePlugin
26
25
  });
27
26
  module.exports = __toCommonJS(index_exports);
28
27
 
28
+ // src/realtime-service-plugin.ts
29
+ var import_audit = require("@objectstack/platform-objects/audit");
30
+
29
31
  // src/in-memory-realtime-adapter.ts
30
32
  var InMemoryRealtimeAdapter = class {
31
33
  constructor(options = {}) {
@@ -101,100 +103,6 @@ var InMemoryRealtimeAdapter = class {
101
103
  }
102
104
  };
103
105
 
104
- // src/objects/sys-presence.object.ts
105
- var import_data = require("@objectstack/spec/data");
106
- var SysPresence = import_data.ObjectSchema.create({
107
- namespace: "sys",
108
- name: "presence",
109
- label: "Presence",
110
- pluralLabel: "Presences",
111
- icon: "wifi",
112
- isSystem: true,
113
- description: "Real-time user presence and activity tracking",
114
- titleFormat: "{user_id} ({status})",
115
- compactLayout: ["user_id", "status", "last_seen"],
116
- fields: {
117
- id: import_data.Field.text({
118
- label: "Presence ID",
119
- required: true,
120
- readonly: true
121
- }),
122
- created_at: import_data.Field.datetime({
123
- label: "Created At",
124
- defaultValue: "NOW()",
125
- readonly: true
126
- }),
127
- updated_at: import_data.Field.datetime({
128
- label: "Updated At",
129
- defaultValue: "NOW()",
130
- readonly: true
131
- }),
132
- user_id: import_data.Field.text({
133
- label: "User ID",
134
- required: true,
135
- searchable: true
136
- }),
137
- session_id: import_data.Field.text({
138
- label: "Session ID",
139
- required: true
140
- }),
141
- status: import_data.Field.select({
142
- label: "Status",
143
- required: true,
144
- defaultValue: "online",
145
- options: [
146
- { value: "online", label: "Online" },
147
- { value: "away", label: "Away" },
148
- { value: "busy", label: "Busy" },
149
- { value: "offline", label: "Offline" }
150
- ]
151
- }),
152
- last_seen: import_data.Field.datetime({
153
- label: "Last Seen",
154
- required: true,
155
- defaultValue: "NOW()"
156
- }),
157
- current_location: import_data.Field.text({
158
- label: "Current Location",
159
- required: false,
160
- maxLength: 500
161
- }),
162
- device: import_data.Field.select({
163
- label: "Device",
164
- required: false,
165
- options: [
166
- { value: "desktop", label: "Desktop" },
167
- { value: "mobile", label: "Mobile" },
168
- { value: "tablet", label: "Tablet" },
169
- { value: "other", label: "Other" }
170
- ]
171
- }),
172
- custom_status: import_data.Field.text({
173
- label: "Custom Status",
174
- required: false,
175
- maxLength: 255
176
- }),
177
- metadata: import_data.Field.json({
178
- label: "Metadata",
179
- required: false,
180
- description: "Arbitrary JSON metadata associated with the presence state (matches PresenceStateSchema.metadata)."
181
- })
182
- },
183
- indexes: [
184
- { fields: ["user_id"], unique: false },
185
- { fields: ["session_id"], unique: true },
186
- { fields: ["status"], unique: false }
187
- ],
188
- enable: {
189
- trackHistory: false,
190
- searchable: false,
191
- apiEnabled: true,
192
- apiMethods: ["get", "list", "create", "update", "delete"],
193
- trash: false,
194
- mru: false
195
- }
196
- });
197
-
198
106
  // src/realtime-service-plugin.ts
199
107
  var RealtimeServicePlugin = class {
200
108
  constructor(options = {}) {
@@ -213,7 +121,7 @@ var RealtimeServicePlugin = class {
213
121
  version: "1.0.0",
214
122
  type: "plugin",
215
123
  namespace: "sys",
216
- objects: [SysPresence]
124
+ objects: [import_audit.SysPresence]
217
125
  });
218
126
  ctx.logger.info("RealtimeServicePlugin: registered in-memory realtime adapter");
219
127
  }
@@ -221,7 +129,6 @@ var RealtimeServicePlugin = class {
221
129
  // Annotate the CommonJS export names for ESM import in node:
222
130
  0 && (module.exports = {
223
131
  InMemoryRealtimeAdapter,
224
- RealtimeServicePlugin,
225
- SysPresence
132
+ RealtimeServicePlugin
226
133
  });
227
134
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/in-memory-realtime-adapter.ts","../src/objects/sys-presence.object.ts","../src/realtime-service-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { RealtimeServicePlugin } from './realtime-service-plugin.js';\nexport type { RealtimeServicePluginOptions } from './realtime-service-plugin.js';\nexport { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter.js';\nexport type { InMemoryRealtimeAdapterOptions } from './in-memory-realtime-adapter.js';\nexport { SysPresence } from './objects/index.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IRealtimeService,\n RealtimeEventPayload,\n RealtimeEventHandler,\n RealtimeSubscriptionOptions,\n} from '@objectstack/spec/contracts';\n\n/**\n * Internal subscription entry.\n */\ninterface Subscription {\n id: string;\n channel: string;\n handler: RealtimeEventHandler;\n options?: RealtimeSubscriptionOptions;\n}\n\n/**\n * Configuration options for InMemoryRealtimeAdapter.\n */\nexport interface InMemoryRealtimeAdapterOptions {\n /** Maximum number of subscriptions allowed (0 = unlimited) */\n maxSubscriptions?: number;\n}\n\n/**\n * In-memory pub/sub adapter implementing IRealtimeService.\n *\n * Uses a Map-backed subscription store with channel-based routing.\n * Supports event type and object filtering via subscription options.\n *\n * Suitable for single-process environments, development, and testing.\n * For production multi-instance deployments, use a Redis-backed adapter.\n *\n * @example\n * ```ts\n * const realtime = new InMemoryRealtimeAdapter();\n *\n * const subId = await realtime.subscribe('records', (event) => {\n * console.log('Received:', event.type, event.payload);\n * }, { object: 'account', eventTypes: ['record.created'] });\n *\n * await realtime.publish({\n * type: 'record.created',\n * object: 'account',\n * payload: { id: 'acc-1', name: 'Acme' },\n * timestamp: new Date().toISOString(),\n * });\n *\n * await realtime.unsubscribe(subId);\n * ```\n */\nexport class InMemoryRealtimeAdapter implements IRealtimeService {\n private readonly subscriptions = new Map<string, Subscription>();\n private readonly channelIndex = new Map<string, Set<string>>();\n private counter = 0;\n private readonly maxSubscriptions: number;\n\n constructor(options: InMemoryRealtimeAdapterOptions = {}) {\n this.maxSubscriptions = options.maxSubscriptions ?? 0;\n }\n\n async publish(event: RealtimeEventPayload): Promise<void> {\n // Deliver to all channel subscriptions that match filters\n for (const sub of this.subscriptions.values()) {\n if (this.matchesSubscription(event, sub)) {\n try {\n await sub.handler(event);\n } catch {\n // Swallow handler errors to avoid breaking the publish loop\n }\n }\n }\n }\n\n async subscribe(\n channel: string,\n handler: RealtimeEventHandler,\n options?: RealtimeSubscriptionOptions,\n ): Promise<string> {\n if (this.maxSubscriptions > 0 && this.subscriptions.size >= this.maxSubscriptions) {\n throw new Error(\n `Maximum subscription limit reached (${this.maxSubscriptions}). ` +\n 'Unsubscribe from existing channels before adding new subscriptions.',\n );\n }\n\n const id = `sub-${++this.counter}`;\n const sub: Subscription = { id, channel, handler, options };\n this.subscriptions.set(id, sub);\n\n // Maintain channel index for efficient lookups\n if (!this.channelIndex.has(channel)) {\n this.channelIndex.set(channel, new Set());\n }\n this.channelIndex.get(channel)!.add(id);\n\n return id;\n }\n\n async unsubscribe(subscriptionId: string): Promise<void> {\n const sub = this.subscriptions.get(subscriptionId);\n if (!sub) return;\n\n this.subscriptions.delete(subscriptionId);\n\n // Clean up channel index\n const channelSubs = this.channelIndex.get(sub.channel);\n if (channelSubs) {\n channelSubs.delete(subscriptionId);\n if (channelSubs.size === 0) {\n this.channelIndex.delete(sub.channel);\n }\n }\n }\n\n /**\n * Get the number of active subscriptions.\n */\n getSubscriptionCount(): number {\n return this.subscriptions.size;\n }\n\n /**\n * Get all active channel names.\n */\n getChannels(): string[] {\n return Array.from(this.channelIndex.keys());\n }\n\n /**\n * Check if an event matches a subscription's filters.\n */\n private matchesSubscription(event: RealtimeEventPayload, sub: Subscription): boolean {\n const opts = sub.options;\n if (!opts) return true;\n\n // Filter by object name\n if (opts.object && event.object !== opts.object) {\n return false;\n }\n\n // Filter by event types\n if (opts.eventTypes && opts.eventTypes.length > 0) {\n if (!opts.eventTypes.includes(event.type)) {\n return false;\n }\n }\n\n return true;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_presence — System Presence Object\n *\n * Tracks real-time user presence and activity across the platform.\n * Fields align with the PresenceStateSchema protocol definition\n * from `@objectstack/spec/api` (websocket.zod.ts).\n *\n * Owned by `service-realtime` as the canonical Presence domain object.\n *\n * @namespace sys\n * @see PresenceStateSchema in packages/spec/src/api/websocket.zod.ts\n */\nexport const SysPresence = ObjectSchema.create({\n namespace: 'sys',\n name: 'presence',\n label: 'Presence',\n pluralLabel: 'Presences',\n icon: 'wifi',\n isSystem: true,\n description: 'Real-time user presence and activity tracking',\n titleFormat: '{user_id} ({status})',\n compactLayout: ['user_id', 'status', 'last_seen'],\n\n fields: {\n id: Field.text({\n label: 'Presence ID',\n required: true,\n readonly: true,\n }),\n\n created_at: Field.datetime({\n label: 'Created At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n\n updated_at: Field.datetime({\n label: 'Updated At',\n defaultValue: 'NOW()',\n readonly: true,\n }),\n\n user_id: Field.text({\n label: 'User ID',\n required: true,\n searchable: true,\n }),\n\n session_id: Field.text({\n label: 'Session ID',\n required: true,\n }),\n\n status: Field.select({\n label: 'Status',\n required: true,\n defaultValue: 'online',\n options: [\n { value: 'online', label: 'Online' },\n { value: 'away', label: 'Away' },\n { value: 'busy', label: 'Busy' },\n { value: 'offline', label: 'Offline' },\n ],\n }),\n\n last_seen: Field.datetime({\n label: 'Last Seen',\n required: true,\n defaultValue: 'NOW()',\n }),\n\n current_location: Field.text({\n label: 'Current Location',\n required: false,\n maxLength: 500,\n }),\n\n device: Field.select({\n label: 'Device',\n required: false,\n options: [\n { value: 'desktop', label: 'Desktop' },\n { value: 'mobile', label: 'Mobile' },\n { value: 'tablet', label: 'Tablet' },\n { value: 'other', label: 'Other' },\n ],\n }),\n\n custom_status: Field.text({\n label: 'Custom Status',\n required: false,\n maxLength: 255,\n }),\n\n metadata: Field.json({\n label: 'Metadata',\n required: false,\n description: 'Arbitrary JSON metadata associated with the presence state (matches PresenceStateSchema.metadata).',\n }),\n },\n\n indexes: [\n { fields: ['user_id'], unique: false },\n { fields: ['session_id'], unique: true },\n { fields: ['status'], unique: false },\n ],\n\n enable: {\n trackHistory: false,\n searchable: false,\n apiEnabled: true,\n apiMethods: ['get', 'list', 'create', 'update', 'delete'],\n trash: false,\n mru: false,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter.js';\nimport type { InMemoryRealtimeAdapterOptions } from './in-memory-realtime-adapter.js';\nimport { SysPresence } from './objects/index.js';\n\n/**\n * Configuration options for the RealtimeServicePlugin.\n */\nexport interface RealtimeServicePluginOptions {\n /** Realtime adapter type (default: 'memory') */\n adapter?: 'memory';\n /** Options for the in-memory adapter */\n memory?: InMemoryRealtimeAdapterOptions;\n}\n\n/**\n * RealtimeServicePlugin — Production IRealtimeService implementation.\n *\n * Registers a realtime pub/sub service with the kernel during the init phase.\n * Currently supports in-memory pub/sub for single-process environments.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { RealtimeServicePlugin } from '@objectstack/service-realtime';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new RealtimeServicePlugin());\n * await kernel.bootstrap();\n *\n * const realtime = kernel.getService('realtime');\n * await realtime.subscribe('records', (event) => {\n * console.log(event.type, event.payload);\n * });\n * ```\n */\nexport class RealtimeServicePlugin implements Plugin {\n name = 'com.objectstack.service.realtime';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: RealtimeServicePluginOptions;\n\n constructor(options: RealtimeServicePluginOptions = {}) {\n this.options = { adapter: 'memory', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const realtime = new InMemoryRealtimeAdapter(this.options.memory);\n ctx.registerService('realtime', realtime);\n\n // Register realtime system objects via the manifest service.\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.realtime',\n name: 'Realtime Service',\n version: '1.0.0',\n type: 'plugin',\n namespace: 'sys',\n objects: [SysPresence],\n });\n\n ctx.logger.info('RealtimeServicePlugin: registered in-memory realtime adapter');\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/realtime-service-plugin.ts","../src/in-memory-realtime-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { RealtimeServicePlugin } from './realtime-service-plugin.js';\nexport type { RealtimeServicePluginOptions } from './realtime-service-plugin.js';\nexport { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter.js';\nexport type { InMemoryRealtimeAdapterOptions } from './in-memory-realtime-adapter.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysPresence } from '@objectstack/platform-objects/audit';\nimport { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter.js';\nimport type { InMemoryRealtimeAdapterOptions } from './in-memory-realtime-adapter.js';\n\n/**\n * Configuration options for the RealtimeServicePlugin.\n */\nexport interface RealtimeServicePluginOptions {\n /** Realtime adapter type (default: 'memory') */\n adapter?: 'memory';\n /** Options for the in-memory adapter */\n memory?: InMemoryRealtimeAdapterOptions;\n}\n\n/**\n * RealtimeServicePlugin — Production IRealtimeService implementation.\n *\n * Registers a realtime pub/sub service with the kernel during the init phase.\n * Currently supports in-memory pub/sub for single-process environments.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { RealtimeServicePlugin } from '@objectstack/service-realtime';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new RealtimeServicePlugin());\n * await kernel.bootstrap();\n *\n * const realtime = kernel.getService('realtime');\n * await realtime.subscribe('records', (event) => {\n * console.log(event.type, event.payload);\n * });\n * ```\n */\nexport class RealtimeServicePlugin implements Plugin {\n name = 'com.objectstack.service.realtime';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: RealtimeServicePluginOptions;\n\n constructor(options: RealtimeServicePluginOptions = {}) {\n this.options = { adapter: 'memory', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const realtime = new InMemoryRealtimeAdapter(this.options.memory);\n ctx.registerService('realtime', realtime);\n\n // Register realtime system objects via the manifest service.\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.realtime',\n name: 'Realtime Service',\n version: '1.0.0',\n type: 'plugin',\n namespace: 'sys',\n objects: [SysPresence],\n });\n\n ctx.logger.info('RealtimeServicePlugin: registered in-memory realtime adapter');\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IRealtimeService,\n RealtimeEventPayload,\n RealtimeEventHandler,\n RealtimeSubscriptionOptions,\n} from '@objectstack/spec/contracts';\n\n/**\n * Internal subscription entry.\n */\ninterface Subscription {\n id: string;\n channel: string;\n handler: RealtimeEventHandler;\n options?: RealtimeSubscriptionOptions;\n}\n\n/**\n * Configuration options for InMemoryRealtimeAdapter.\n */\nexport interface InMemoryRealtimeAdapterOptions {\n /** Maximum number of subscriptions allowed (0 = unlimited) */\n maxSubscriptions?: number;\n}\n\n/**\n * In-memory pub/sub adapter implementing IRealtimeService.\n *\n * Uses a Map-backed subscription store with channel-based routing.\n * Supports event type and object filtering via subscription options.\n *\n * Suitable for single-process environments, development, and testing.\n * For production multi-instance deployments, use a Redis-backed adapter.\n *\n * @example\n * ```ts\n * const realtime = new InMemoryRealtimeAdapter();\n *\n * const subId = await realtime.subscribe('records', (event) => {\n * console.log('Received:', event.type, event.payload);\n * }, { object: 'account', eventTypes: ['record.created'] });\n *\n * await realtime.publish({\n * type: 'record.created',\n * object: 'account',\n * payload: { id: 'acc-1', name: 'Acme' },\n * timestamp: new Date().toISOString(),\n * });\n *\n * await realtime.unsubscribe(subId);\n * ```\n */\nexport class InMemoryRealtimeAdapter implements IRealtimeService {\n private readonly subscriptions = new Map<string, Subscription>();\n private readonly channelIndex = new Map<string, Set<string>>();\n private counter = 0;\n private readonly maxSubscriptions: number;\n\n constructor(options: InMemoryRealtimeAdapterOptions = {}) {\n this.maxSubscriptions = options.maxSubscriptions ?? 0;\n }\n\n async publish(event: RealtimeEventPayload): Promise<void> {\n // Deliver to all channel subscriptions that match filters\n for (const sub of this.subscriptions.values()) {\n if (this.matchesSubscription(event, sub)) {\n try {\n await sub.handler(event);\n } catch {\n // Swallow handler errors to avoid breaking the publish loop\n }\n }\n }\n }\n\n async subscribe(\n channel: string,\n handler: RealtimeEventHandler,\n options?: RealtimeSubscriptionOptions,\n ): Promise<string> {\n if (this.maxSubscriptions > 0 && this.subscriptions.size >= this.maxSubscriptions) {\n throw new Error(\n `Maximum subscription limit reached (${this.maxSubscriptions}). ` +\n 'Unsubscribe from existing channels before adding new subscriptions.',\n );\n }\n\n const id = `sub-${++this.counter}`;\n const sub: Subscription = { id, channel, handler, options };\n this.subscriptions.set(id, sub);\n\n // Maintain channel index for efficient lookups\n if (!this.channelIndex.has(channel)) {\n this.channelIndex.set(channel, new Set());\n }\n this.channelIndex.get(channel)!.add(id);\n\n return id;\n }\n\n async unsubscribe(subscriptionId: string): Promise<void> {\n const sub = this.subscriptions.get(subscriptionId);\n if (!sub) return;\n\n this.subscriptions.delete(subscriptionId);\n\n // Clean up channel index\n const channelSubs = this.channelIndex.get(sub.channel);\n if (channelSubs) {\n channelSubs.delete(subscriptionId);\n if (channelSubs.size === 0) {\n this.channelIndex.delete(sub.channel);\n }\n }\n }\n\n /**\n * Get the number of active subscriptions.\n */\n getSubscriptionCount(): number {\n return this.subscriptions.size;\n }\n\n /**\n * Get all active channel names.\n */\n getChannels(): string[] {\n return Array.from(this.channelIndex.keys());\n }\n\n /**\n * Check if an event matches a subscription's filters.\n */\n private matchesSubscription(event: RealtimeEventPayload, sub: Subscription): boolean {\n const opts = sub.options;\n if (!opts) return true;\n\n // Filter by object name\n if (opts.object && event.object !== opts.object) {\n return false;\n }\n\n // Filter by event types\n if (opts.eventTypes && opts.eventTypes.length > 0) {\n if (!opts.eventTypes.includes(event.type)) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"mappings":"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","names":[]}