@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/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/realtime-service-plugin.ts
2
+ import { SysPresence } from "@objectstack/platform-objects/audit";
3
+
1
4
  // src/in-memory-realtime-adapter.ts
2
5
  var InMemoryRealtimeAdapter = class {
3
6
  constructor(options = {}) {
@@ -73,100 +76,6 @@ var InMemoryRealtimeAdapter = class {
73
76
  }
74
77
  };
75
78
 
76
- // src/objects/sys-presence.object.ts
77
- import { ObjectSchema, Field } from "@objectstack/spec/data";
78
- var SysPresence = ObjectSchema.create({
79
- namespace: "sys",
80
- name: "presence",
81
- label: "Presence",
82
- pluralLabel: "Presences",
83
- icon: "wifi",
84
- isSystem: true,
85
- description: "Real-time user presence and activity tracking",
86
- titleFormat: "{user_id} ({status})",
87
- compactLayout: ["user_id", "status", "last_seen"],
88
- fields: {
89
- id: Field.text({
90
- label: "Presence ID",
91
- required: true,
92
- readonly: true
93
- }),
94
- created_at: Field.datetime({
95
- label: "Created At",
96
- defaultValue: "NOW()",
97
- readonly: true
98
- }),
99
- updated_at: Field.datetime({
100
- label: "Updated At",
101
- defaultValue: "NOW()",
102
- readonly: true
103
- }),
104
- user_id: Field.text({
105
- label: "User ID",
106
- required: true,
107
- searchable: true
108
- }),
109
- session_id: Field.text({
110
- label: "Session ID",
111
- required: true
112
- }),
113
- status: Field.select({
114
- label: "Status",
115
- required: true,
116
- defaultValue: "online",
117
- options: [
118
- { value: "online", label: "Online" },
119
- { value: "away", label: "Away" },
120
- { value: "busy", label: "Busy" },
121
- { value: "offline", label: "Offline" }
122
- ]
123
- }),
124
- last_seen: Field.datetime({
125
- label: "Last Seen",
126
- required: true,
127
- defaultValue: "NOW()"
128
- }),
129
- current_location: Field.text({
130
- label: "Current Location",
131
- required: false,
132
- maxLength: 500
133
- }),
134
- device: Field.select({
135
- label: "Device",
136
- required: false,
137
- options: [
138
- { value: "desktop", label: "Desktop" },
139
- { value: "mobile", label: "Mobile" },
140
- { value: "tablet", label: "Tablet" },
141
- { value: "other", label: "Other" }
142
- ]
143
- }),
144
- custom_status: Field.text({
145
- label: "Custom Status",
146
- required: false,
147
- maxLength: 255
148
- }),
149
- metadata: Field.json({
150
- label: "Metadata",
151
- required: false,
152
- description: "Arbitrary JSON metadata associated with the presence state (matches PresenceStateSchema.metadata)."
153
- })
154
- },
155
- indexes: [
156
- { fields: ["user_id"], unique: false },
157
- { fields: ["session_id"], unique: true },
158
- { fields: ["status"], unique: false }
159
- ],
160
- enable: {
161
- trackHistory: false,
162
- searchable: false,
163
- apiEnabled: true,
164
- apiMethods: ["get", "list", "create", "update", "delete"],
165
- trash: false,
166
- mru: false
167
- }
168
- });
169
-
170
79
  // src/realtime-service-plugin.ts
171
80
  var RealtimeServicePlugin = class {
172
81
  constructor(options = {}) {
@@ -192,7 +101,6 @@ var RealtimeServicePlugin = class {
192
101
  };
193
102
  export {
194
103
  InMemoryRealtimeAdapter,
195
- RealtimeServicePlugin,
196
- SysPresence
104
+ RealtimeServicePlugin
197
105
  };
198
106
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../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\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/realtime-service-plugin.ts","../src/in-memory-realtime-adapter.ts"],"sourcesContent":["// 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":";AAGA,SAAS,mBAAmB;;;ACmDrB,IAAM,0BAAN,MAA0D;AAAA,EAM/D,YAAY,UAA0C,CAAC,GAAG;AAL1D,SAAiB,gBAAgB,oBAAI,IAA0B;AAC/D,SAAiB,eAAe,oBAAI,IAAyB;AAC7D,SAAQ,UAAU;AAIhB,SAAK,mBAAmB,QAAQ,oBAAoB;AAAA,EACtD;AAAA,EAEA,MAAM,QAAQ,OAA4C;AAExD,eAAW,OAAO,KAAK,cAAc,OAAO,GAAG;AAC7C,UAAI,KAAK,oBAAoB,OAAO,GAAG,GAAG;AACxC,YAAI;AACF,gBAAM,IAAI,QAAQ,KAAK;AAAA,QACzB,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,UACJ,SACA,SACA,SACiB;AACjB,QAAI,KAAK,mBAAmB,KAAK,KAAK,cAAc,QAAQ,KAAK,kBAAkB;AACjF,YAAM,IAAI;AAAA,QACR,uCAAuC,KAAK,gBAAgB;AAAA,MAE9D;AAAA,IACF;AAEA,UAAM,KAAK,OAAO,EAAE,KAAK,OAAO;AAChC,UAAM,MAAoB,EAAE,IAAI,SAAS,SAAS,QAAQ;AAC1D,SAAK,cAAc,IAAI,IAAI,GAAG;AAG9B,QAAI,CAAC,KAAK,aAAa,IAAI,OAAO,GAAG;AACnC,WAAK,aAAa,IAAI,SAAS,oBAAI,IAAI,CAAC;AAAA,IAC1C;AACA,SAAK,aAAa,IAAI,OAAO,EAAG,IAAI,EAAE;AAEtC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,gBAAuC;AACvD,UAAM,MAAM,KAAK,cAAc,IAAI,cAAc;AACjD,QAAI,CAAC,IAAK;AAEV,SAAK,cAAc,OAAO,cAAc;AAGxC,UAAM,cAAc,KAAK,aAAa,IAAI,IAAI,OAAO;AACrD,QAAI,aAAa;AACf,kBAAY,OAAO,cAAc;AACjC,UAAI,YAAY,SAAS,GAAG;AAC1B,aAAK,aAAa,OAAO,IAAI,OAAO;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,uBAA+B;AAC7B,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,cAAwB;AACtB,WAAO,MAAM,KAAK,KAAK,aAAa,KAAK,CAAC;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAKQ,oBAAoB,OAA6B,KAA4B;AACnF,UAAM,OAAO,IAAI;AACjB,QAAI,CAAC,KAAM,QAAO;AAGlB,QAAI,KAAK,UAAU,MAAM,WAAW,KAAK,QAAQ;AAC/C,aAAO;AAAA,IACT;AAGA,QAAI,KAAK,cAAc,KAAK,WAAW,SAAS,GAAG;AACjD,UAAI,CAAC,KAAK,WAAW,SAAS,MAAM,IAAI,GAAG;AACzC,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;ADnHO,IAAM,wBAAN,MAA8C;AAAA,EAQnD,YAAY,UAAwC,CAAC,GAAG;AAPxD,gBAAO;AACP,mBAAU;AACV,gBAAO;AACP,wBAAe,CAAC,iCAAiC;AAK/C,SAAK,UAAU,EAAE,SAAS,UAAU,GAAG,QAAQ;AAAA,EACjD;AAAA,EAEA,MAAM,KAAK,KAAmC;AAC5C,UAAM,WAAW,IAAI,wBAAwB,KAAK,QAAQ,MAAM;AAChE,QAAI,gBAAgB,YAAY,QAAQ;AAGxC,QAAI,WAAuC,UAAU,EAAE,SAAS;AAAA,MAC9D,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,WAAW;AAAA,MACX,SAAS,CAAC,WAAW;AAAA,IACvB,CAAC;AAED,QAAI,OAAO,KAAK,8DAA8D;AAAA,EAChF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-realtime",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Realtime Service for ObjectStack — implements IRealtimeService with WebSocket and in-memory pub/sub",
6
6
  "type": "module",
@@ -14,13 +14,39 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@objectstack/core": "4.0.3",
18
- "@objectstack/spec": "4.0.3"
17
+ "@objectstack/core": "4.0.5",
18
+ "@objectstack/platform-objects": "4.0.5",
19
+ "@objectstack/spec": "4.0.5"
19
20
  },
20
21
  "devDependencies": {
21
- "@types/node": "^25.6.0",
22
- "typescript": "^6.0.2",
23
- "vitest": "^4.1.4"
22
+ "@types/node": "^25.6.2",
23
+ "typescript": "^6.0.3",
24
+ "vitest": "^4.1.5"
25
+ },
26
+ "keywords": [
27
+ "objectstack",
28
+ "service",
29
+ "realtime",
30
+ "websocket",
31
+ "sse"
32
+ ],
33
+ "author": "ObjectStack",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/objectstack-ai/framework.git",
37
+ "directory": "packages/services/service-realtime"
38
+ },
39
+ "homepage": "https://objectstack.ai/docs",
40
+ "bugs": "https://github.com/objectstack-ai/framework/issues",
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md"
47
+ ],
48
+ "engines": {
49
+ "node": ">=18.0.0"
24
50
  },
25
51
  "scripts": {
26
52
  "build": "tsup --config ../../../tsup.config.ts",
@@ -1,22 +0,0 @@
1
-
2
- > @objectstack/service-realtime@4.0.3 build /home/runner/work/framework/framework/packages/services/service-realtime
3
- > tsup --config ../../../tsup.config.ts
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
9
- CLI Target: es2020
10
- CLI Cleaning output folder
11
- ESM Build start
12
- CJS Build start
13
- CJS dist/index.cjs 6.64 KB
14
- CJS dist/index.cjs.map 13.93 KB
15
- CJS ⚡️ Build success in 71ms
16
- ESM dist/index.js 5.40 KB
17
- ESM dist/index.js.map 13.42 KB
18
- ESM ⚡️ Build success in 75ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 16178ms
21
- DTS dist/index.d.ts 99.42 KB
22
- DTS dist/index.d.cts 99.42 KB
package/CHANGELOG.md DELETED
@@ -1,160 +0,0 @@
1
- # @objectstack/service-realtime
2
-
3
- ## 4.0.3
4
-
5
- ### Patch Changes
6
-
7
- - @objectstack/spec@4.0.3
8
- - @objectstack/core@4.0.3
9
-
10
- ## 4.0.2
11
-
12
- ### Patch Changes
13
-
14
- - Updated dependencies [5f659e9]
15
- - @objectstack/spec@4.0.2
16
- - @objectstack/core@4.0.2
17
-
18
- ## 4.0.0
19
-
20
- ### Patch Changes
21
-
22
- - Updated dependencies [f08ffc3]
23
- - Updated dependencies [e0b0a78]
24
- - @objectstack/spec@4.0.0
25
- - @objectstack/core@4.0.0
26
-
27
- ## 3.3.1
28
-
29
- ### Patch Changes
30
-
31
- - @objectstack/spec@3.3.1
32
- - @objectstack/core@3.3.1
33
-
34
- ## 3.3.0
35
-
36
- ### Patch Changes
37
-
38
- - @objectstack/spec@3.3.0
39
- - @objectstack/core@3.3.0
40
-
41
- ## 3.2.9
42
-
43
- ### Patch Changes
44
-
45
- - @objectstack/spec@3.2.9
46
- - @objectstack/core@3.2.9
47
-
48
- ## 3.2.8
49
-
50
- ### Patch Changes
51
-
52
- - @objectstack/spec@3.2.8
53
- - @objectstack/core@3.2.8
54
-
55
- ## 3.2.7
56
-
57
- ### Patch Changes
58
-
59
- - @objectstack/spec@3.2.7
60
- - @objectstack/core@3.2.7
61
-
62
- ## 3.2.6
63
-
64
- ### Patch Changes
65
-
66
- - @objectstack/spec@3.2.6
67
- - @objectstack/core@3.2.6
68
-
69
- ## 3.2.5
70
-
71
- ### Patch Changes
72
-
73
- - @objectstack/spec@3.2.5
74
- - @objectstack/core@3.2.5
75
-
76
- ## 3.2.4
77
-
78
- ### Patch Changes
79
-
80
- - @objectstack/spec@3.2.4
81
- - @objectstack/core@3.2.4
82
-
83
- ## 3.2.3
84
-
85
- ### Patch Changes
86
-
87
- - @objectstack/spec@3.2.3
88
- - @objectstack/core@3.2.3
89
-
90
- ## 3.2.2
91
-
92
- ### Patch Changes
93
-
94
- - Updated dependencies [46defbb]
95
- - @objectstack/spec@3.2.2
96
- - @objectstack/core@3.2.2
97
-
98
- ## 3.2.1
99
-
100
- ### Patch Changes
101
-
102
- - Updated dependencies [850b546]
103
- - @objectstack/spec@3.2.1
104
- - @objectstack/core@3.2.1
105
-
106
- ## 3.2.0
107
-
108
- ### Patch Changes
109
-
110
- - Updated dependencies [5901c29]
111
- - @objectstack/spec@3.2.0
112
- - @objectstack/core@3.2.0
113
-
114
- ## 3.1.1
115
-
116
- ### Patch Changes
117
-
118
- - Updated dependencies [953d667]
119
- - @objectstack/spec@3.1.1
120
- - @objectstack/core@3.1.1
121
-
122
- ## 3.1.0
123
-
124
- ### Patch Changes
125
-
126
- - Updated dependencies [0088830]
127
- - @objectstack/spec@3.1.0
128
- - @objectstack/core@3.1.0
129
-
130
- ## 3.0.11
131
-
132
- ### Patch Changes
133
-
134
- - Updated dependencies [92d9d99]
135
- - @objectstack/spec@3.0.11
136
- - @objectstack/core@3.0.11
137
-
138
- ## 3.0.10
139
-
140
- ### Patch Changes
141
-
142
- - Updated dependencies [d1e5d31]
143
- - @objectstack/spec@3.0.10
144
- - @objectstack/core@3.0.10
145
-
146
- ## 3.0.9
147
-
148
- ### Patch Changes
149
-
150
- - Updated dependencies [15e0df6]
151
- - @objectstack/spec@3.0.9
152
- - @objectstack/core@3.0.9
153
-
154
- ## 3.0.8
155
-
156
- ### Patch Changes
157
-
158
- - Updated dependencies [5a968a2]
159
- - @objectstack/spec@3.0.8
160
- - @objectstack/core@3.0.8
@@ -1,242 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect } from 'vitest';
4
- import { InMemoryRealtimeAdapter } from './in-memory-realtime-adapter';
5
- import type { IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';
6
-
7
- describe('InMemoryRealtimeAdapter', () => {
8
- it('should implement IRealtimeService contract', () => {
9
- const realtime: IRealtimeService = new InMemoryRealtimeAdapter();
10
- expect(typeof realtime.publish).toBe('function');
11
- expect(typeof realtime.subscribe).toBe('function');
12
- expect(typeof realtime.unsubscribe).toBe('function');
13
- });
14
-
15
- it('should start with zero subscriptions', () => {
16
- const realtime = new InMemoryRealtimeAdapter();
17
- expect(realtime.getSubscriptionCount()).toBe(0);
18
- expect(realtime.getChannels()).toEqual([]);
19
- });
20
-
21
- it('should subscribe and receive published events', async () => {
22
- const realtime = new InMemoryRealtimeAdapter();
23
- const received: RealtimeEventPayload[] = [];
24
-
25
- await realtime.subscribe('records', (event) => {
26
- received.push(event);
27
- });
28
-
29
- await realtime.publish({
30
- type: 'record.created',
31
- object: 'account',
32
- payload: { id: 'acc-1', name: 'Acme' },
33
- timestamp: new Date().toISOString(),
34
- });
35
-
36
- expect(received).toHaveLength(1);
37
- expect(received[0].type).toBe('record.created');
38
- expect(received[0].object).toBe('account');
39
- });
40
-
41
- it('should deliver events to multiple subscribers', async () => {
42
- const realtime = new InMemoryRealtimeAdapter();
43
- const received1: RealtimeEventPayload[] = [];
44
- const received2: RealtimeEventPayload[] = [];
45
-
46
- await realtime.subscribe('records', (event) => { received1.push(event); });
47
- await realtime.subscribe('records', (event) => { received2.push(event); });
48
-
49
- await realtime.publish({
50
- type: 'record.created',
51
- object: 'account',
52
- payload: { id: 'acc-1' },
53
- timestamp: new Date().toISOString(),
54
- });
55
-
56
- expect(received1).toHaveLength(1);
57
- expect(received2).toHaveLength(1);
58
- });
59
-
60
- it('should unsubscribe and stop receiving events', async () => {
61
- const realtime = new InMemoryRealtimeAdapter();
62
- const received: RealtimeEventPayload[] = [];
63
-
64
- const subId = await realtime.subscribe('records', (event) => {
65
- received.push(event);
66
- });
67
-
68
- await realtime.publish({
69
- type: 'record.created',
70
- payload: { id: '1' },
71
- timestamp: new Date().toISOString(),
72
- });
73
- expect(received).toHaveLength(1);
74
-
75
- await realtime.unsubscribe(subId);
76
-
77
- await realtime.publish({
78
- type: 'record.updated',
79
- payload: { id: '2' },
80
- timestamp: new Date().toISOString(),
81
- });
82
- expect(received).toHaveLength(1); // No new events
83
- });
84
-
85
- it('should handle unsubscribing an unknown subscription gracefully', async () => {
86
- const realtime = new InMemoryRealtimeAdapter();
87
- await expect(realtime.unsubscribe('nonexistent')).resolves.toBeUndefined();
88
- });
89
-
90
- it('should filter events by object name', async () => {
91
- const realtime = new InMemoryRealtimeAdapter();
92
- const received: RealtimeEventPayload[] = [];
93
-
94
- await realtime.subscribe('records', (event) => {
95
- received.push(event);
96
- }, { object: 'account' });
97
-
98
- await realtime.publish({
99
- type: 'record.created',
100
- object: 'account',
101
- payload: { id: '1' },
102
- timestamp: new Date().toISOString(),
103
- });
104
-
105
- await realtime.publish({
106
- type: 'record.created',
107
- object: 'contact',
108
- payload: { id: '2' },
109
- timestamp: new Date().toISOString(),
110
- });
111
-
112
- expect(received).toHaveLength(1);
113
- expect(received[0].object).toBe('account');
114
- });
115
-
116
- it('should filter events by event type', async () => {
117
- const realtime = new InMemoryRealtimeAdapter();
118
- const received: RealtimeEventPayload[] = [];
119
-
120
- await realtime.subscribe('records', (event) => {
121
- received.push(event);
122
- }, { eventTypes: ['record.created'] });
123
-
124
- await realtime.publish({
125
- type: 'record.created',
126
- payload: { id: '1' },
127
- timestamp: new Date().toISOString(),
128
- });
129
-
130
- await realtime.publish({
131
- type: 'record.updated',
132
- payload: { id: '2' },
133
- timestamp: new Date().toISOString(),
134
- });
135
-
136
- expect(received).toHaveLength(1);
137
- expect(received[0].type).toBe('record.created');
138
- });
139
-
140
- it('should filter by both object and event type', async () => {
141
- const realtime = new InMemoryRealtimeAdapter();
142
- const received: RealtimeEventPayload[] = [];
143
-
144
- await realtime.subscribe('records', (event) => {
145
- received.push(event);
146
- }, { object: 'account', eventTypes: ['record.created'] });
147
-
148
- // Match: correct object + correct type
149
- await realtime.publish({
150
- type: 'record.created',
151
- object: 'account',
152
- payload: { id: '1' },
153
- timestamp: new Date().toISOString(),
154
- });
155
-
156
- // No match: wrong object
157
- await realtime.publish({
158
- type: 'record.created',
159
- object: 'contact',
160
- payload: { id: '2' },
161
- timestamp: new Date().toISOString(),
162
- });
163
-
164
- // No match: wrong type
165
- await realtime.publish({
166
- type: 'record.updated',
167
- object: 'account',
168
- payload: { id: '3' },
169
- timestamp: new Date().toISOString(),
170
- });
171
-
172
- expect(received).toHaveLength(1);
173
- expect(received[0].payload).toEqual({ id: '1' });
174
- });
175
-
176
- it('should track subscription count and channels', async () => {
177
- const realtime = new InMemoryRealtimeAdapter();
178
-
179
- const sub1 = await realtime.subscribe('records', () => {});
180
- await realtime.subscribe('events', () => {});
181
-
182
- expect(realtime.getSubscriptionCount()).toBe(2);
183
- expect(realtime.getChannels().sort()).toEqual(['events', 'records']);
184
-
185
- await realtime.unsubscribe(sub1);
186
- expect(realtime.getSubscriptionCount()).toBe(1);
187
- expect(realtime.getChannels()).toEqual(['events']);
188
- });
189
-
190
- it('should enforce maxSubscriptions limit', async () => {
191
- const realtime = new InMemoryRealtimeAdapter({ maxSubscriptions: 2 });
192
-
193
- await realtime.subscribe('ch1', () => {});
194
- await realtime.subscribe('ch2', () => {});
195
-
196
- await expect(realtime.subscribe('ch3', () => {})).rejects.toThrow(
197
- /Maximum subscription limit reached/,
198
- );
199
- });
200
-
201
- it('should not break publish loop on handler error', async () => {
202
- const realtime = new InMemoryRealtimeAdapter();
203
- const received: RealtimeEventPayload[] = [];
204
-
205
- await realtime.subscribe('records', () => {
206
- throw new Error('handler error');
207
- });
208
- await realtime.subscribe('records', (event) => {
209
- received.push(event);
210
- });
211
-
212
- await realtime.publish({
213
- type: 'record.created',
214
- payload: { id: '1' },
215
- timestamp: new Date().toISOString(),
216
- });
217
-
218
- // Second handler should still receive the event
219
- expect(received).toHaveLength(1);
220
- });
221
-
222
- it('should return unique subscription IDs', async () => {
223
- const realtime = new InMemoryRealtimeAdapter();
224
-
225
- const id1 = await realtime.subscribe('ch1', () => {});
226
- const id2 = await realtime.subscribe('ch1', () => {});
227
- const id3 = await realtime.subscribe('ch2', () => {});
228
-
229
- expect(id1).not.toBe(id2);
230
- expect(id2).not.toBe(id3);
231
- });
232
-
233
- it('should clean up channel index on last subscription removal', async () => {
234
- const realtime = new InMemoryRealtimeAdapter();
235
-
236
- const sub1 = await realtime.subscribe('records', () => {});
237
- expect(realtime.getChannels()).toContain('records');
238
-
239
- await realtime.unsubscribe(sub1);
240
- expect(realtime.getChannels()).not.toContain('records');
241
- });
242
- });