@fitzzero/quickdraw-core 3.1.0 → 3.3.0
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/client/index.d.ts +3 -3
- package/dist/client/index.js +89 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.d.ts +1 -1
- package/dist/client/testing.js +2 -0
- package/dist/client/testing.js.map +1 -1
- package/dist/server/index.d.ts +35 -2
- package/dist/server/index.js +134 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/testing.d.ts +1 -1
- package/dist/server/testing.js +48 -0
- package/dist/server/testing.js.map +1 -1
- package/dist/{types-e6WPGE70.d.ts → types-Cr8YC7Kg.d.ts} +22 -1
- package/dist/{types-D7oHWh4O.d.ts → types-DniMxkQs.d.ts} +1 -0
- package/package.json +1 -1
package/dist/client/testing.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { QueryClient } from '@tanstack/react-query';
|
|
3
3
|
import { ServiceResponse } from '../shared/index.js';
|
|
4
|
-
import { Q as QuickdrawSocketContextValue } from '../types-
|
|
4
|
+
import { Q as QuickdrawSocketContextValue } from '../types-Cr8YC7Kg.js';
|
|
5
5
|
import 'zod';
|
|
6
6
|
import 'socket.io-client';
|
|
7
7
|
|
package/dist/client/testing.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/testing.tsx"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["import * as React from \"react\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { QuickdrawSocketContextValue } from \"./types\";\n\n// Generic mock function type that works with both Jest and Vitest\ntype MockFn = (...args: unknown[]) => unknown;\n\n/**\n * Mock socket for testing client components.\n */\nexport interface MockSocket {\n id: string;\n connected: boolean;\n emit: MockFn;\n on: MockFn;\n off: MockFn;\n disconnect: MockFn;\n}\n\n/**\n * Create a mock socket for testing.\n */\nexport function createMockSocket(): MockSocket {\n // Simple mock function that can be enhanced by the test framework\n const createMock = (): MockFn => {\n const fn: MockFn = () => undefined;\n return fn;\n };\n\n return {\n id: \"mock-socket-id\",\n connected: true,\n emit: createMock(),\n on: createMock(),\n off: createMock(),\n disconnect: createMock(),\n };\n}\n\n/**\n * Create a mock subscription registry for testing.\n */\nfunction createMockSubscriptionRegistry() {\n const subscriptions = new Map<string, { refCount: number; cleanup?: () => void }>();\n \n return {\n acquire: (key: string) => {\n const existing = subscriptions.get(key);\n if (existing) {\n existing.refCount++;\n return { isNew: false, entry: existing };\n }\n const entry = { refCount: 1 };\n subscriptions.set(key, entry);\n return { isNew: true, entry };\n },\n release: (key: string) => {\n const entry = subscriptions.get(key);\n if (!entry) return false;\n entry.refCount--;\n if (entry.refCount <= 0) {\n entry.cleanup?.();\n subscriptions.delete(key);\n return true;\n }\n return false;\n },\n setCleanup: (key: string, cleanup: () => void) => {\n const entry = subscriptions.get(key);\n if (entry) entry.cleanup = cleanup;\n },\n clear: () => subscriptions.clear(),\n };\n}\n\n/**\n * Create a mock QuickdrawSocketContextValue for testing.\n */\nexport function createMockSocketContext(\n overrides: Partial<QuickdrawSocketContextValue> = {}\n): QuickdrawSocketContextValue {\n const mockSocket = createMockSocket();\n\n return {\n socket: mockSocket as unknown as Socket,\n isConnected: true,\n userId: \"test-user-id\",\n serviceAccess: {},\n connect: () => {},\n disconnect: () => {},\n subscriptionRegistry: createMockSubscriptionRegistry(),\n ...overrides,\n };\n}\n\n/**\n * Mock socket emit that resolves with a successful response.\n */\nexport function mockSuccessEmit<T>(data: T): (\n _event: string,\n _payload: unknown,\n callback?: (response: ServiceResponse<T>) => void\n) => void {\n return (_event, _payload, callback) => {\n callback?.({ success: true, data });\n };\n}\n\n/**\n * Mock socket emit that resolves with an error response.\n */\nexport function mockErrorEmit(\n error: string,\n code?: number\n): (\n _event: string,\n _payload: unknown,\n callback?: (response: ServiceResponse<unknown>) => void\n) => void {\n return (_event, _payload, callback) => {\n const response: ServiceResponse<unknown> = { success: false, error };\n if (code !== undefined) {\n (response as { success: false; error: string; code: number }).code = code;\n }\n callback?.(response);\n };\n}\n\n/**\n * Create a mock QueryClient for testing.\n */\nexport function createMockQueryClient(): QueryClient {\n // Dynamic import to avoid bundling issues\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { QueryClient } = require(\"@tanstack/react-query\") as {\n QueryClient: new (options?: unknown) => QueryClient;\n };\n\n return new QueryClient({\n defaultOptions: {\n queries: {\n retry: false,\n gcTime: 0,\n },\n mutations: {\n retry: false,\n },\n },\n });\n}\n\n/**\n * Test wrapper component that provides the necessary context.\n */\nexport interface TestWrapperProps {\n children: React.ReactNode;\n socketContext?: Partial<QuickdrawSocketContextValue>;\n queryClient?: QueryClient;\n}\n\n/**\n * Create a test wrapper for rendering components with quickdraw context.\n *\n * @example\n * ```tsx\n * import { render } from '@testing-library/react';\n * import { createTestWrapper, createMockSocketContext, mockSuccessEmit } from '@quickdraw/core/client/testing';\n *\n * test('creates chat', async () => {\n * const mockSocket = createMockSocket();\n * mockSocket.emit.mockImplementation(mockSuccessEmit({ id: 'chat-123' }));\n *\n * const wrapper = createTestWrapper({\n * socketContext: { socket: mockSocket },\n * });\n *\n * const { getByText } = render(<CreateChatButton />, { wrapper });\n * // ...\n * });\n * ```\n */\nexport function createTestWrapper(\n options: Omit<TestWrapperProps, \"children\"> = {}\n): ({ children }: { children: React.ReactNode }) => React.ReactElement {\n const socketContext = createMockSocketContext(options.socketContext);\n const queryClient = options.queryClient ?? createMockQueryClient();\n\n // Create context for testing\n const QuickdrawSocketContext = React.createContext<QuickdrawSocketContextValue | null>(null);\n\n return function TestWrapper({ children }: { children: React.ReactNode }): React.ReactElement {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { QueryClientProvider } = require(\"@tanstack/react-query\") as {\n QueryClientProvider: React.ComponentType<{ client: QueryClient; children: React.ReactNode }>;\n };\n\n return (\n <QueryClientProvider client={queryClient}>\n <QuickdrawSocketContext.Provider value={socketContext}>\n {children}\n </QuickdrawSocketContext.Provider>\n </QueryClientProvider>\n );\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/client/testing.tsx"],"names":[],"mappings":";;;;;;;;;AAwBO,SAAS,gBAAA,GAA+B;AAE7C,EAAA,MAAM,aAAa,MAAc;AAC/B,IAAA,MAAM,KAAa,MAAM,MAAA;AACzB,IAAA,OAAO,EAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,gBAAA;AAAA,IACJ,SAAA,EAAW,IAAA;AAAA,IACX,MAAM,UAAA,EAAW;AAAA,IACjB,IAAI,UAAA,EAAW;AAAA,IACf,KAAK,UAAA,EAAW;AAAA,IAChB,YAAY,UAAA;AAAW,GACzB;AACF;AAKA,SAAS,8BAAA,GAAiC;AACxC,EAAA,MAAM,aAAA,uBAAoB,GAAA,EAAwD;AAElF,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAC,GAAA,KAAgB;AACxB,MAAA,MAAM,QAAA,GAAW,aAAA,CAAc,GAAA,CAAI,GAAG,CAAA;AACtC,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,QAAA,CAAS,QAAA,EAAA;AACT,QAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,QAAA,EAAS;AAAA,MACzC;AACA,MAAA,MAAM,KAAA,GAAQ,EAAE,QAAA,EAAU,CAAA,EAAE;AAC5B,MAAA,aAAA,CAAc,GAAA,CAAI,KAAK,KAAK,CAAA;AAC5B,MAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,KAAA,EAAM;AAAA,IAC9B,CAAA;AAAA,IACA,OAAA,EAAS,CAAC,GAAA,KAAgB;AACxB,MAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,GAAA,CAAI,GAAG,CAAA;AACnC,MAAA,IAAI,CAAC,OAAO,OAAO,KAAA;AACnB,MAAA,KAAA,CAAM,QAAA,EAAA;AACN,MAAA,IAAI,KAAA,CAAM,YAAY,CAAA,EAAG;AACvB,QAAA,KAAA,CAAM,OAAA,IAAU;AAChB,QAAA,aAAA,CAAc,OAAO,GAAG,CAAA;AACxB,QAAA,OAAO,IAAA;AAAA,MACT;AACA,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AAAA,IACA,UAAA,EAAY,CAAC,GAAA,EAAa,OAAA,KAAwB;AAChD,MAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,GAAA,CAAI,GAAG,CAAA;AACnC,MAAA,IAAI,KAAA,QAAa,OAAA,GAAU,OAAA;AAAA,IAC7B,CAAA;AAAA,IACA,KAAA,EAAO,MAAM,aAAA,CAAc,KAAA;AAAM,GACnC;AACF;AAKO,SAAS,uBAAA,CACd,SAAA,GAAkD,EAAC,EACtB;AAC7B,EAAA,MAAM,aAAa,gBAAA,EAAiB;AAEpC,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,UAAA;AAAA,IACR,WAAA,EAAa,IAAA;AAAA,IACb,MAAA,EAAQ,cAAA;AAAA,IACR,eAAe,EAAC;AAAA,IAChB,SAAS,MAAM;AAAA,IAAC,CAAA;AAAA,IAChB,YAAY,MAAM;AAAA,IAAC,CAAA;AAAA,IACnB,sBAAsB,8BAAA,EAA+B;AAAA,IACrD,mBAAA,EAAqB,EAAE,OAAA,EAAS,MAAM;AAAA,IAAC,CAAA,EAAE;AAAA,IACzC,GAAG;AAAA,GACL;AACF;AAKO,SAAS,gBAAmB,IAAA,EAIzB;AACR,EAAA,OAAO,CAAC,MAAA,EAAQ,QAAA,EAAU,QAAA,KAAa;AACrC,IAAA,QAAA,GAAW,EAAE,OAAA,EAAS,IAAA,EAAM,IAAA,EAAM,CAAA;AAAA,EACpC,CAAA;AACF;AAKO,SAAS,aAAA,CACd,OACA,IAAA,EAKQ;AACR,EAAA,OAAO,CAAC,MAAA,EAAQ,QAAA,EAAU,QAAA,KAAa;AACrC,IAAA,MAAM,QAAA,GAAqC,EAAE,OAAA,EAAS,KAAA,EAAO,KAAA,EAAM;AACnE,IAAA,IAAI,SAAS,MAAA,EAAW;AACtB,MAAC,SAA6D,IAAA,GAAO,IAAA;AAAA,IACvE;AACA,IAAA,QAAA,GAAW,QAAQ,CAAA;AAAA,EACrB,CAAA;AACF;AAKO,SAAS,qBAAA,GAAqC;AAGnD,EAAA,MAAM,EAAE,WAAA,EAAY,GAAI,SAAA,CAAQ,uBAAuB,CAAA;AAIvD,EAAA,OAAO,IAAI,WAAA,CAAY;AAAA,IACrB,cAAA,EAAgB;AAAA,MACd,OAAA,EAAS;AAAA,QACP,KAAA,EAAO,KAAA;AAAA,QACP,MAAA,EAAQ;AAAA,OACV;AAAA,MACA,SAAA,EAAW;AAAA,QACT,KAAA,EAAO;AAAA;AACT;AACF,GACD,CAAA;AACH;AAgCO,SAAS,iBAAA,CACd,OAAA,GAA8C,EAAC,EACsB;AACrE,EAAA,MAAM,aAAA,GAAgB,uBAAA,CAAwB,OAAA,CAAQ,aAAa,CAAA;AACnE,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,qBAAA,EAAsB;AAGjE,EAAA,MAAM,sBAAA,GAA+B,oBAAkD,IAAI,CAAA;AAE3F,EAAA,OAAO,SAAS,WAAA,CAAY,EAAE,QAAA,EAAS,EAAsD;AAE3F,IAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,SAAA,CAAQ,uBAAuB,CAAA;AAI/D,IAAA,uBACE,GAAA,CAAC,mBAAA,EAAA,EAAoB,MAAA,EAAQ,WAAA,EAC3B,QAAA,kBAAA,GAAA,CAAC,sBAAA,CAAuB,QAAA,EAAvB,EAAgC,KAAA,EAAO,aAAA,EACrC,QAAA,EACH,CAAA,EACF,CAAA;AAAA,EAEJ,CAAA;AACF","file":"testing.js","sourcesContent":["import * as React from \"react\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { QuickdrawSocketContextValue } from \"./types\";\n\n// Generic mock function type that works with both Jest and Vitest\ntype MockFn = (...args: unknown[]) => unknown;\n\n/**\n * Mock socket for testing client components.\n */\nexport interface MockSocket {\n id: string;\n connected: boolean;\n emit: MockFn;\n on: MockFn;\n off: MockFn;\n disconnect: MockFn;\n}\n\n/**\n * Create a mock socket for testing.\n */\nexport function createMockSocket(): MockSocket {\n // Simple mock function that can be enhanced by the test framework\n const createMock = (): MockFn => {\n const fn: MockFn = () => undefined;\n return fn;\n };\n\n return {\n id: \"mock-socket-id\",\n connected: true,\n emit: createMock(),\n on: createMock(),\n off: createMock(),\n disconnect: createMock(),\n };\n}\n\n/**\n * Create a mock subscription registry for testing.\n */\nfunction createMockSubscriptionRegistry() {\n const subscriptions = new Map<string, { refCount: number; cleanup?: () => void }>();\n \n return {\n acquire: (key: string) => {\n const existing = subscriptions.get(key);\n if (existing) {\n existing.refCount++;\n return { isNew: false, entry: existing };\n }\n const entry = { refCount: 1 };\n subscriptions.set(key, entry);\n return { isNew: true, entry };\n },\n release: (key: string) => {\n const entry = subscriptions.get(key);\n if (!entry) return false;\n entry.refCount--;\n if (entry.refCount <= 0) {\n entry.cleanup?.();\n subscriptions.delete(key);\n return true;\n }\n return false;\n },\n setCleanup: (key: string, cleanup: () => void) => {\n const entry = subscriptions.get(key);\n if (entry) entry.cleanup = cleanup;\n },\n clear: () => subscriptions.clear(),\n };\n}\n\n/**\n * Create a mock QuickdrawSocketContextValue for testing.\n */\nexport function createMockSocketContext(\n overrides: Partial<QuickdrawSocketContextValue> = {}\n): QuickdrawSocketContextValue {\n const mockSocket = createMockSocket();\n\n return {\n socket: mockSocket as unknown as Socket,\n isConnected: true,\n userId: \"test-user-id\",\n serviceAccess: {},\n connect: () => {},\n disconnect: () => {},\n subscriptionRegistry: createMockSubscriptionRegistry(),\n subscriptionBatcher: { enqueue: () => {} },\n ...overrides,\n };\n}\n\n/**\n * Mock socket emit that resolves with a successful response.\n */\nexport function mockSuccessEmit<T>(data: T): (\n _event: string,\n _payload: unknown,\n callback?: (response: ServiceResponse<T>) => void\n) => void {\n return (_event, _payload, callback) => {\n callback?.({ success: true, data });\n };\n}\n\n/**\n * Mock socket emit that resolves with an error response.\n */\nexport function mockErrorEmit(\n error: string,\n code?: number\n): (\n _event: string,\n _payload: unknown,\n callback?: (response: ServiceResponse<unknown>) => void\n) => void {\n return (_event, _payload, callback) => {\n const response: ServiceResponse<unknown> = { success: false, error };\n if (code !== undefined) {\n (response as { success: false; error: string; code: number }).code = code;\n }\n callback?.(response);\n };\n}\n\n/**\n * Create a mock QueryClient for testing.\n */\nexport function createMockQueryClient(): QueryClient {\n // Dynamic import to avoid bundling issues\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { QueryClient } = require(\"@tanstack/react-query\") as {\n QueryClient: new (options?: unknown) => QueryClient;\n };\n\n return new QueryClient({\n defaultOptions: {\n queries: {\n retry: false,\n gcTime: 0,\n },\n mutations: {\n retry: false,\n },\n },\n });\n}\n\n/**\n * Test wrapper component that provides the necessary context.\n */\nexport interface TestWrapperProps {\n children: React.ReactNode;\n socketContext?: Partial<QuickdrawSocketContextValue>;\n queryClient?: QueryClient;\n}\n\n/**\n * Create a test wrapper for rendering components with quickdraw context.\n *\n * @example\n * ```tsx\n * import { render } from '@testing-library/react';\n * import { createTestWrapper, createMockSocketContext, mockSuccessEmit } from '@quickdraw/core/client/testing';\n *\n * test('creates chat', async () => {\n * const mockSocket = createMockSocket();\n * mockSocket.emit.mockImplementation(mockSuccessEmit({ id: 'chat-123' }));\n *\n * const wrapper = createTestWrapper({\n * socketContext: { socket: mockSocket },\n * });\n *\n * const { getByText } = render(<CreateChatButton />, { wrapper });\n * // ...\n * });\n * ```\n */\nexport function createTestWrapper(\n options: Omit<TestWrapperProps, \"children\"> = {}\n): ({ children }: { children: React.ReactNode }) => React.ReactElement {\n const socketContext = createMockSocketContext(options.socketContext);\n const queryClient = options.queryClient ?? createMockQueryClient();\n\n // Create context for testing\n const QuickdrawSocketContext = React.createContext<QuickdrawSocketContextValue | null>(null);\n\n return function TestWrapper({ children }: { children: React.ReactNode }): React.ReactElement {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { QueryClientProvider } = require(\"@tanstack/react-query\") as {\n QueryClientProvider: React.ComponentType<{ client: QueryClient; children: React.ReactNode }>;\n };\n\n return (\n <QueryClientProvider client={queryClient}>\n <QuickdrawSocketContext.Provider value={socketContext}>\n {children}\n </QuickdrawSocketContext.Provider>\n </QueryClientProvider>\n );\n };\n}\n"]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as QuickdrawSocket, P as PrismaDelegate, b as BaseServiceOptions, I as InstallAdminMethodsOptions, A as AdminListPayload, c as AdminListResponse, d as AdminSetACLPayload, e as AdminSubscribersResponse, S as ServiceRegistryInstance, B as BaseServiceInstance, f as QuickdrawServerOptions, Q as QuickdrawServerResult } from '../types-
|
|
1
|
+
import { a as QuickdrawSocket, P as PrismaDelegate, b as BaseServiceOptions, I as InstallAdminMethodsOptions, A as AdminListPayload, c as AdminListResponse, d as AdminSetACLPayload, e as AdminSubscribersResponse, S as ServiceRegistryInstance, B as BaseServiceInstance, f as QuickdrawServerOptions, Q as QuickdrawServerResult } from '../types-DniMxkQs.js';
|
|
2
2
|
import { Server, Socket } from 'socket.io';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ACL, Logger, AccessLevel, ServiceMethodContext, ServiceMethodDefinition, AdminServiceMeta, AdminFieldConfig } from '../shared/index.js';
|
|
@@ -84,6 +84,31 @@ declare abstract class BaseService<TEntity extends {
|
|
|
84
84
|
* Also joins the socket to the entity's Socket.io room for cross-service events.
|
|
85
85
|
*/
|
|
86
86
|
subscribe(entryId: string, socket: QuickdrawSocket, requiredLevel?: AccessLevel): Promise<TEntity | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Subscribe a socket to multiple entities at once.
|
|
89
|
+
* Batches ACL checks and entity fetches for efficiency.
|
|
90
|
+
* Override `checkBatchSubscriptionAccess` and `findByIds` in derived classes
|
|
91
|
+
* for optimized batch queries (e.g., single findMany instead of N findUnique).
|
|
92
|
+
*
|
|
93
|
+
* Default implementation runs individual checks/fetches in parallel via Promise.all.
|
|
94
|
+
*/
|
|
95
|
+
batchSubscribe(entryIds: string[], socket: QuickdrawSocket, requiredLevel?: AccessLevel): Promise<Record<string, TEntity | null>>;
|
|
96
|
+
/**
|
|
97
|
+
* Batch ACL check for multiple entities.
|
|
98
|
+
* Override in derived classes for efficient batch queries
|
|
99
|
+
* (e.g., one findMany + one membership check per project instead of per entity).
|
|
100
|
+
*
|
|
101
|
+
* Default: checks each entity individually in parallel.
|
|
102
|
+
*/
|
|
103
|
+
protected checkBatchSubscriptionAccess(userId: string, entryIds: string[], requiredLevel: AccessLevel, socket: QuickdrawSocket): Promise<Map<string, boolean>>;
|
|
104
|
+
/**
|
|
105
|
+
* Batch entity fetch by IDs.
|
|
106
|
+
* Override in derived classes for efficient batch queries
|
|
107
|
+
* (e.g., one findMany with includes instead of N findUnique).
|
|
108
|
+
*
|
|
109
|
+
* Default: fetches each entity individually in parallel.
|
|
110
|
+
*/
|
|
111
|
+
protected findByIds(ids: string[]): Promise<TEntity[]>;
|
|
87
112
|
/**
|
|
88
113
|
* Unsubscribe a socket from an entity's updates.
|
|
89
114
|
*/
|
|
@@ -228,9 +253,12 @@ declare abstract class BaseService<TEntity extends {
|
|
|
228
253
|
/**
|
|
229
254
|
* Define a public method that will be exposed via Socket.io.
|
|
230
255
|
*
|
|
256
|
+
* Can be called from external helper functions to split method definitions
|
|
257
|
+
* across multiple files while keeping full type safety.
|
|
258
|
+
*
|
|
231
259
|
* @typeParam K - Method name from the service methods map
|
|
232
260
|
*/
|
|
233
|
-
|
|
261
|
+
defineMethod<K extends keyof TServiceMethods & string>(name: K, access: AccessLevel, handler: (payload: TServiceMethods[K]["payload"], context: ServiceMethodContext) => Promise<TServiceMethods[K]["response"]>, options?: {
|
|
234
262
|
schema?: z.ZodType<TServiceMethods[K]["payload"]>;
|
|
235
263
|
resolveEntryId?: (payload: TServiceMethods[K]["payload"]) => string | null;
|
|
236
264
|
}): ServiceMethodDefinition<TServiceMethods[K]["payload"], TServiceMethods[K]["response"]>;
|
|
@@ -406,6 +434,11 @@ declare class ServiceRegistry implements ServiceRegistryInstance {
|
|
|
406
434
|
* Register subscription listener for a service.
|
|
407
435
|
*/
|
|
408
436
|
private registerSubscriptionListener;
|
|
437
|
+
/**
|
|
438
|
+
* Register batch subscription listener for a service.
|
|
439
|
+
* Handles subscribing to multiple entities in a single event.
|
|
440
|
+
*/
|
|
441
|
+
private registerBatchSubscriptionListener;
|
|
409
442
|
/**
|
|
410
443
|
* Register unsubscription listener for a service.
|
|
411
444
|
*/
|
package/dist/server/index.js
CHANGED
|
@@ -265,6 +265,89 @@ var BaseService = class {
|
|
|
265
265
|
if (!entity) return null;
|
|
266
266
|
return this.filterEntityForSubscriber(entity, socket, entryId);
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Subscribe a socket to multiple entities at once.
|
|
270
|
+
* Batches ACL checks and entity fetches for efficiency.
|
|
271
|
+
* Override `checkBatchSubscriptionAccess` and `findByIds` in derived classes
|
|
272
|
+
* for optimized batch queries (e.g., single findMany instead of N findUnique).
|
|
273
|
+
*
|
|
274
|
+
* Default implementation runs individual checks/fetches in parallel via Promise.all.
|
|
275
|
+
*/
|
|
276
|
+
async batchSubscribe(entryIds, socket, requiredLevel = "Read") {
|
|
277
|
+
if (!socket.userId || entryIds.length === 0) {
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
const accessMap = await this.checkBatchSubscriptionAccess(
|
|
281
|
+
socket.userId,
|
|
282
|
+
entryIds,
|
|
283
|
+
requiredLevel,
|
|
284
|
+
socket
|
|
285
|
+
);
|
|
286
|
+
const allowedIds = entryIds.filter((id) => accessMap.get(id) === true);
|
|
287
|
+
const entities = allowedIds.length > 0 ? await this.findByIds(allowedIds) : [];
|
|
288
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const entity of entities) {
|
|
290
|
+
entityMap.set(entity.id, entity);
|
|
291
|
+
}
|
|
292
|
+
const results = {};
|
|
293
|
+
for (const entryId of entryIds) {
|
|
294
|
+
const entity = entityMap.get(entryId);
|
|
295
|
+
if (!entity || !accessMap.get(entryId)) {
|
|
296
|
+
results[entryId] = null;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!this.subscribers.has(entryId)) {
|
|
300
|
+
this.subscribers.set(entryId, /* @__PURE__ */ new Set());
|
|
301
|
+
}
|
|
302
|
+
this.subscribers.get(entryId).add(socket);
|
|
303
|
+
const roomName = this.getRoomName(entryId);
|
|
304
|
+
void socket.join(roomName);
|
|
305
|
+
results[entryId] = this.filterEntityForSubscriber(
|
|
306
|
+
entity,
|
|
307
|
+
socket,
|
|
308
|
+
entryId
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
this.logger.debug(
|
|
312
|
+
`User ${socket.userId} batch-subscribed to ${allowedIds.length}/${entryIds.length} entities`
|
|
313
|
+
);
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Batch ACL check for multiple entities.
|
|
318
|
+
* Override in derived classes for efficient batch queries
|
|
319
|
+
* (e.g., one findMany + one membership check per project instead of per entity).
|
|
320
|
+
*
|
|
321
|
+
* Default: checks each entity individually in parallel.
|
|
322
|
+
*/
|
|
323
|
+
async checkBatchSubscriptionAccess(userId, entryIds, requiredLevel, socket) {
|
|
324
|
+
const results = /* @__PURE__ */ new Map();
|
|
325
|
+
await Promise.all(
|
|
326
|
+
entryIds.map(async (id) => {
|
|
327
|
+
const allowed = await this.checkSubscriptionAccess(
|
|
328
|
+
userId,
|
|
329
|
+
id,
|
|
330
|
+
requiredLevel,
|
|
331
|
+
socket
|
|
332
|
+
);
|
|
333
|
+
results.set(id, allowed);
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
return results;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Batch entity fetch by IDs.
|
|
340
|
+
* Override in derived classes for efficient batch queries
|
|
341
|
+
* (e.g., one findMany with includes instead of N findUnique).
|
|
342
|
+
*
|
|
343
|
+
* Default: fetches each entity individually in parallel.
|
|
344
|
+
*/
|
|
345
|
+
async findByIds(ids) {
|
|
346
|
+
const results = await Promise.all(
|
|
347
|
+
ids.map((id) => this.findById(id))
|
|
348
|
+
);
|
|
349
|
+
return results.filter((e) => e !== null);
|
|
350
|
+
}
|
|
268
351
|
/**
|
|
269
352
|
* Unsubscribe a socket from an entity's updates.
|
|
270
353
|
*/
|
|
@@ -584,6 +667,9 @@ var BaseService = class {
|
|
|
584
667
|
/**
|
|
585
668
|
* Define a public method that will be exposed via Socket.io.
|
|
586
669
|
*
|
|
670
|
+
* Can be called from external helper functions to split method definitions
|
|
671
|
+
* across multiple files while keeping full type safety.
|
|
672
|
+
*
|
|
587
673
|
* @typeParam K - Method name from the service methods map
|
|
588
674
|
*/
|
|
589
675
|
defineMethod(name, access, handler, options) {
|
|
@@ -1001,6 +1087,11 @@ var ServiceRegistry = class {
|
|
|
1001
1087
|
`${serviceName}:subscribe`,
|
|
1002
1088
|
service
|
|
1003
1089
|
);
|
|
1090
|
+
this.registerBatchSubscriptionListener(
|
|
1091
|
+
socket,
|
|
1092
|
+
`${serviceName}:batchSubscribe`,
|
|
1093
|
+
service
|
|
1094
|
+
);
|
|
1004
1095
|
this.registerUnsubscriptionListener(
|
|
1005
1096
|
socket,
|
|
1006
1097
|
`${serviceName}:unsubscribe`,
|
|
@@ -1194,6 +1285,49 @@ var ServiceRegistry = class {
|
|
|
1194
1285
|
}
|
|
1195
1286
|
);
|
|
1196
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Register batch subscription listener for a service.
|
|
1290
|
+
* Handles subscribing to multiple entities in a single event.
|
|
1291
|
+
*/
|
|
1292
|
+
registerBatchSubscriptionListener(socket, eventName, service) {
|
|
1293
|
+
socket.on(
|
|
1294
|
+
eventName,
|
|
1295
|
+
async (payload, callback) => {
|
|
1296
|
+
try {
|
|
1297
|
+
if (!socket.userId) {
|
|
1298
|
+
callback?.({
|
|
1299
|
+
success: false,
|
|
1300
|
+
error: "Authentication required",
|
|
1301
|
+
code: 401
|
|
1302
|
+
});
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (!Array.isArray(payload.entryIds) || payload.entryIds.length === 0) {
|
|
1306
|
+
callback?.({
|
|
1307
|
+
success: false,
|
|
1308
|
+
error: "entryIds must be a non-empty array",
|
|
1309
|
+
code: 400
|
|
1310
|
+
});
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const data = await service.batchSubscribe(
|
|
1314
|
+
payload.entryIds,
|
|
1315
|
+
socket,
|
|
1316
|
+
payload.requiredLevel ?? "Read"
|
|
1317
|
+
);
|
|
1318
|
+
callback?.({ success: true, data });
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
this.logger.error(`Error in batch subscription ${eventName}:`, {
|
|
1321
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1322
|
+
});
|
|
1323
|
+
callback?.({
|
|
1324
|
+
success: false,
|
|
1325
|
+
error: error instanceof Error ? error.message : "Batch subscription failed"
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1197
1331
|
/**
|
|
1198
1332
|
* Register unsubscription listener for a service.
|
|
1199
1333
|
*/
|