@fitzzero/quickdraw-core 3.3.1 → 3.5.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.
@@ -93,6 +93,16 @@ interface UseSubscriptionResult<TData> {
93
93
  subscribe: () => void;
94
94
  unsubscribe: () => void;
95
95
  }
96
+ /**
97
+ * Options for the useRoomEvents hook.
98
+ */
99
+ interface UseRoomEventsOptions {
100
+ /**
101
+ * Whether to listen for events (default: true).
102
+ * Set to false to temporarily disable all listeners.
103
+ */
104
+ enabled?: boolean;
105
+ }
96
106
  /**
97
107
  * Options for the useServiceQuery hook.
98
108
  */
@@ -145,6 +155,22 @@ interface UseServiceQueryOptions<TResponse> {
145
155
  * Delay between retries in milliseconds
146
156
  */
147
157
  retryDelay?: number;
158
+ /**
159
+ * Socket event names that should trigger an automatic refetch.
160
+ * When any of these events fire on the socket, the query is refetched.
161
+ * Useful for keeping list queries in sync with real-time changes.
162
+ *
163
+ * Rapid-fire events within the same 100ms window are debounced into a single refetch.
164
+ *
165
+ * @example
166
+ * ```tsx
167
+ * // Refetch the task list when tasks are created, deleted, or change status
168
+ * useServiceQuery("taskService", "listTasks", { projectId }, {
169
+ * invalidateOn: ["task:created", "task:deleted", "task:statusUpdate"],
170
+ * });
171
+ * ```
172
+ */
173
+ invalidateOn?: string[];
148
174
  }
149
175
  /**
150
176
  * Return type for the useServiceQuery hook.
@@ -259,4 +285,4 @@ interface QuickdrawSocketContextValue {
259
285
  subscriptionBatcher: SubscriptionBatcher;
260
286
  }
261
287
 
262
- export type { ClientServiceMethodMap as C, QuickdrawSocketContextValue as Q, SubscriptionDataMap as S, UseServiceOptions as U, QuickdrawProviderProps as a, UseServiceResult as b, UseServiceQueryOptions as c, UseServiceQueryResult as d, UseSubscriptionOptions as e, UseSubscriptionResult as f };
288
+ export type { ClientServiceMethodMap as C, QuickdrawSocketContextValue as Q, SubscriptionDataMap as S, UseServiceOptions as U, QuickdrawProviderProps as a, UseServiceResult as b, UseServiceQueryOptions as c, UseServiceQueryResult as d, UseSubscriptionOptions as e, UseSubscriptionResult as f, UseRoomEventsOptions as g };
@@ -127,6 +127,7 @@ export const client = [
127
127
  ...nextVitals,
128
128
  ...base,
129
129
  {
130
+ plugins: { quickdraw: quickdrawPlugin },
130
131
  rules: {
131
132
  "react/react-in-jsx-scope": "off",
132
133
  "react/prop-types": "off",
@@ -144,6 +145,9 @@ export const client = [
144
145
  "@typescript-eslint/no-unsafe-argument": "warn",
145
146
  "@typescript-eslint/no-redundant-type-constituents": "off",
146
147
  "@typescript-eslint/no-unnecessary-type-arguments": "off",
148
+
149
+ "quickdraw/no-raw-socket-on": "warn",
150
+ "quickdraw/no-raw-socket-emit": "warn",
147
151
  },
148
152
  },
149
153
  ];
@@ -2,6 +2,8 @@ import noCrossServiceMutations from "./rules/no-cross-service-mutations.mjs"
2
2
  import requireZodSchema from "./rules/require-zod-schema.mjs"
3
3
  import noServiceMethodRecord from "./rules/no-service-method-record.mjs"
4
4
  import noUnsafePayloadCast from "./rules/no-unsafe-payload-cast.mjs"
5
+ import noRawSocketOn from "./rules/no-raw-socket-on.mjs"
6
+ import noRawSocketEmit from "./rules/no-raw-socket-emit.mjs"
5
7
 
6
8
  /** @type {import('eslint').ESLint.Plugin} */
7
9
  export default {
@@ -10,5 +12,7 @@ export default {
10
12
  "require-zod-schema": requireZodSchema,
11
13
  "no-service-method-record": noServiceMethodRecord,
12
14
  "no-unsafe-payload-cast": noUnsafePayloadCast,
15
+ "no-raw-socket-on": noRawSocketOn,
16
+ "no-raw-socket-emit": noRawSocketEmit,
13
17
  },
14
18
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Allowed event name prefixes that are exempt from this rule.
3
+ * These are documented exceptions for protocols that don't map to BaseService.
4
+ */
5
+ const DEFAULT_ALLOWED_PREFIXES = ["agentRunner:", "projectRunner:"]
6
+
7
+ /**
8
+ * Allowed exact event names that are exempt from this rule.
9
+ */
10
+ const DEFAULT_ALLOWED_EVENTS = new Set(["subscribe", "unsubscribe"])
11
+
12
+ /** @type {import('eslint').Rule.RuleModule} */
13
+ export default {
14
+ meta: {
15
+ type: "problem",
16
+ docs: {
17
+ description:
18
+ "Disallow raw socket.emit() in components. " +
19
+ "Use useService() for mutations, useServiceQuery() for reads, " +
20
+ "or useSubscription() for entity subscriptions.",
21
+ },
22
+ messages: {
23
+ noRawSocketEmit:
24
+ "Avoid raw socket.emit('{{ eventName }}'). " +
25
+ "Use useService() for mutations or useServiceQuery() for reads.",
26
+ noRawSocketEmitDynamic:
27
+ "Avoid raw socket.emit() with dynamic event names. " +
28
+ "Use useService() for mutations or useServiceQuery() for reads.",
29
+ },
30
+ schema: [
31
+ {
32
+ type: "object",
33
+ properties: {
34
+ allowedPrefixes: {
35
+ type: "array",
36
+ items: { type: "string" },
37
+ description:
38
+ "Event name prefixes that are exempt from this rule " +
39
+ '(e.g. ["agentRunner:", "projectRunner:"])',
40
+ },
41
+ allowedEvents: {
42
+ type: "array",
43
+ items: { type: "string" },
44
+ description:
45
+ "Exact event names that are exempt from this rule " +
46
+ '(e.g. ["subscribe", "unsubscribe"])',
47
+ },
48
+ },
49
+ additionalProperties: false,
50
+ },
51
+ ],
52
+ },
53
+ create(context) {
54
+ const options = context.options[0] || {}
55
+ const allowedPrefixes = options.allowedPrefixes || DEFAULT_ALLOWED_PREFIXES
56
+ const allowedEvents = new Set(
57
+ options.allowedEvents || DEFAULT_ALLOWED_EVENTS
58
+ )
59
+
60
+ return {
61
+ CallExpression(node) {
62
+ const callee = node.callee
63
+ if (callee.type !== "MemberExpression") return
64
+ if (callee.property.type !== "Identifier") return
65
+ if (callee.property.name !== "emit") return
66
+
67
+ const obj = callee.object
68
+ if (obj.type !== "Identifier" || obj.name !== "socket") return
69
+
70
+ const eventArg = node.arguments[0]
71
+ if (!eventArg) return
72
+
73
+ if (eventArg.type === "Literal" && typeof eventArg.value === "string") {
74
+ const eventName = eventArg.value
75
+
76
+ if (allowedEvents.has(eventName)) return
77
+ if (allowedPrefixes.some((prefix) => eventName.startsWith(prefix))) {
78
+ return
79
+ }
80
+
81
+ context.report({
82
+ node,
83
+ messageId: "noRawSocketEmit",
84
+ data: { eventName },
85
+ })
86
+ } else {
87
+ context.report({
88
+ node,
89
+ messageId: "noRawSocketEmitDynamic",
90
+ })
91
+ }
92
+ },
93
+ }
94
+ },
95
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Allowed event name prefixes that are exempt from this rule.
3
+ * These are documented exceptions for protocols that don't map to BaseService.
4
+ */
5
+ const DEFAULT_ALLOWED_PREFIXES = ["agentRunner:", "projectRunner:"]
6
+
7
+ /** @type {import('eslint').Rule.RuleModule} */
8
+ export default {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description:
13
+ "Disallow raw socket.on() in components. " +
14
+ "Use useRoomEvents() for custom event listeners or useSubscription() for entity updates.",
15
+ },
16
+ messages: {
17
+ noRawSocketOn:
18
+ "Avoid raw socket.on('{{ eventName }}'). " +
19
+ "Use useRoomEvents({ '{{ eventName }}': handler }) for lifecycle-managed event listeners, " +
20
+ "or useSubscription() for entity data.",
21
+ noRawSocketOnDynamic:
22
+ "Avoid raw socket.on() with dynamic event names. " +
23
+ "Use useRoomEvents() for lifecycle-managed event listeners.",
24
+ },
25
+ schema: [
26
+ {
27
+ type: "object",
28
+ properties: {
29
+ allowedPrefixes: {
30
+ type: "array",
31
+ items: { type: "string" },
32
+ description:
33
+ "Event name prefixes that are exempt from this rule " +
34
+ '(e.g. ["agentRunner:", "projectRunner:"])',
35
+ },
36
+ },
37
+ additionalProperties: false,
38
+ },
39
+ ],
40
+ },
41
+ create(context) {
42
+ const options = context.options[0] || {}
43
+ const allowedPrefixes = options.allowedPrefixes || DEFAULT_ALLOWED_PREFIXES
44
+
45
+ return {
46
+ CallExpression(node) {
47
+ const callee = node.callee
48
+ if (callee.type !== "MemberExpression") return
49
+ if (callee.property.type !== "Identifier") return
50
+ if (callee.property.name !== "on") return
51
+
52
+ const obj = callee.object
53
+ if (obj.type !== "Identifier" || obj.name !== "socket") return
54
+
55
+ const eventArg = node.arguments[0]
56
+ if (!eventArg) return
57
+
58
+ if (eventArg.type === "Literal" && typeof eventArg.value === "string") {
59
+ const eventName = eventArg.value
60
+
61
+ if (allowedPrefixes.some((prefix) => eventName.startsWith(prefix))) {
62
+ return
63
+ }
64
+
65
+ context.report({
66
+ node,
67
+ messageId: "noRawSocketOn",
68
+ data: { eventName },
69
+ })
70
+ } else {
71
+ context.report({
72
+ node,
73
+ messageId: "noRawSocketOnDynamic",
74
+ })
75
+ }
76
+ },
77
+ }
78
+ },
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitzzero/quickdraw-core",
3
- "version": "3.3.1",
3
+ "version": "3.5.0",
4
4
  "description": "Fast fullstack patterns for real-time applications with Socket.io and TanStack Query",
5
5
  "type": "module",
6
6
  "main": "./dist/shared/index.js",