@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.
@@ -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-e6WPGE70.js';
4
+ import { Q as QuickdrawSocketContextValue } from '../types-Cr8YC7Kg.js';
5
5
  import 'zod';
6
6
  import 'socket.io-client';
7
7
 
@@ -64,6 +64,8 @@ function createMockSocketContext(overrides = {}) {
64
64
  disconnect: () => {
65
65
  },
66
66
  subscriptionRegistry: createMockSubscriptionRegistry(),
67
+ subscriptionBatcher: { enqueue: () => {
68
+ } },
67
69
  ...overrides
68
70
  };
69
71
  }
@@ -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":"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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 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"]}
@@ -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-D7oHWh4O.js';
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
- protected defineMethod<K extends keyof TServiceMethods & string>(name: K, access: AccessLevel, handler: (payload: TServiceMethods[K]["payload"], context: ServiceMethodContext) => Promise<TServiceMethods[K]["response"]>, options?: {
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
  */
@@ -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
  */