@fitzzero/quickdraw-core 3.0.0 → 3.2.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
  */
@@ -406,6 +431,11 @@ declare class ServiceRegistry implements ServiceRegistryInstance {
406
431
  * Register subscription listener for a service.
407
432
  */
408
433
  private registerSubscriptionListener;
434
+ /**
435
+ * Register batch subscription listener for a service.
436
+ * Handles subscribing to multiple entities in a single event.
437
+ */
438
+ private registerBatchSubscriptionListener;
409
439
  /**
410
440
  * Register unsubscription listener for a service.
411
441
  */
@@ -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
  */
@@ -1001,6 +1084,11 @@ var ServiceRegistry = class {
1001
1084
  `${serviceName}:subscribe`,
1002
1085
  service
1003
1086
  );
1087
+ this.registerBatchSubscriptionListener(
1088
+ socket,
1089
+ `${serviceName}:batchSubscribe`,
1090
+ service
1091
+ );
1004
1092
  this.registerUnsubscriptionListener(
1005
1093
  socket,
1006
1094
  `${serviceName}:unsubscribe`,
@@ -1194,6 +1282,49 @@ var ServiceRegistry = class {
1194
1282
  }
1195
1283
  );
1196
1284
  }
1285
+ /**
1286
+ * Register batch subscription listener for a service.
1287
+ * Handles subscribing to multiple entities in a single event.
1288
+ */
1289
+ registerBatchSubscriptionListener(socket, eventName, service) {
1290
+ socket.on(
1291
+ eventName,
1292
+ async (payload, callback) => {
1293
+ try {
1294
+ if (!socket.userId) {
1295
+ callback?.({
1296
+ success: false,
1297
+ error: "Authentication required",
1298
+ code: 401
1299
+ });
1300
+ return;
1301
+ }
1302
+ if (!Array.isArray(payload.entryIds) || payload.entryIds.length === 0) {
1303
+ callback?.({
1304
+ success: false,
1305
+ error: "entryIds must be a non-empty array",
1306
+ code: 400
1307
+ });
1308
+ return;
1309
+ }
1310
+ const data = await service.batchSubscribe(
1311
+ payload.entryIds,
1312
+ socket,
1313
+ payload.requiredLevel ?? "Read"
1314
+ );
1315
+ callback?.({ success: true, data });
1316
+ } catch (error) {
1317
+ this.logger.error(`Error in batch subscription ${eventName}:`, {
1318
+ error: error instanceof Error ? error.message : "Unknown error"
1319
+ });
1320
+ callback?.({
1321
+ success: false,
1322
+ error: error instanceof Error ? error.message : "Batch subscription failed"
1323
+ });
1324
+ }
1325
+ }
1326
+ );
1327
+ }
1197
1328
  /**
1198
1329
  * Register unsubscription listener for a service.
1199
1330
  */