@jskit-ai/users-web 0.1.82 → 0.1.84

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.
@@ -3,7 +3,7 @@ import { HOME_COG_OUTLET } from "./src/shared/toolsOutletContracts.js";
3
3
  export default Object.freeze({
4
4
  packageVersion: 1,
5
5
  packageId: "@jskit-ai/users-web",
6
- version: "0.1.82",
6
+ version: "0.1.84",
7
7
  kind: "runtime",
8
8
  description: "Users web module: account/profile UI plus shared users web widgets.",
9
9
  dependsOn: [
@@ -278,12 +278,12 @@ export default Object.freeze({
278
278
  dependencies: {
279
279
  runtime: {
280
280
  "@mdi/js": "^7.4.47",
281
- "@jskit-ai/http-runtime": "0.1.66",
282
- "@jskit-ai/realtime": "0.1.66",
283
- "@jskit-ai/kernel": "0.1.67",
284
- "@jskit-ai/shell-web": "0.1.66",
285
- "@jskit-ai/uploads-image-web": "0.1.45",
286
- "@jskit-ai/users-core": "0.1.77"
281
+ "@jskit-ai/http-runtime": "0.1.68",
282
+ "@jskit-ai/realtime": "0.1.68",
283
+ "@jskit-ai/kernel": "0.1.69",
284
+ "@jskit-ai/shell-web": "0.1.68",
285
+ "@jskit-ai/uploads-image-web": "0.1.47",
286
+ "@jskit-ai/users-core": "0.1.79"
287
287
  },
288
288
  dev: {}
289
289
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.82",
3
+ "version": "0.1.84",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -44,12 +44,12 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@mdi/js": "^7.4.47",
47
- "@jskit-ai/http-runtime": "0.1.66",
48
- "@jskit-ai/kernel": "0.1.67",
49
- "@jskit-ai/realtime": "0.1.66",
50
- "@jskit-ai/shell-web": "0.1.66",
51
- "@jskit-ai/uploads-image-web": "0.1.45",
52
- "@jskit-ai/users-core": "0.1.77"
47
+ "@jskit-ai/http-runtime": "0.1.68",
48
+ "@jskit-ai/kernel": "0.1.69",
49
+ "@jskit-ai/realtime": "0.1.68",
50
+ "@jskit-ai/shell-web": "0.1.68",
51
+ "@jskit-ai/uploads-image-web": "0.1.47",
52
+ "@jskit-ai/users-core": "0.1.79"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@tanstack/vue-query": "^5.90.5",
@@ -6,6 +6,7 @@ import { useAddEdit } from "./useAddEdit.js";
6
6
  import {
7
7
  resolveCrudBoundValues,
8
8
  } from "../crud/crudBindingSupport.js";
9
+ import { resolveOperationRealtimeOptions } from "../useRealtimeQueryInvalidation.js";
9
10
  import {
10
11
  normalizeCrudFormFields,
11
12
  createCrudFormModel,
@@ -64,6 +65,12 @@ function useCrudAddEdit({
64
65
  operationName
65
66
  }
66
67
  );
68
+ const resolvedRealtime = resolveOperationRealtimeOptions({
69
+ realtime: normalizedAddEditOptions.realtime,
70
+ fallbackRealtime: resolvedResource?.operations?.[operationName]?.realtime ||
71
+ resolvedResource?.operations?.list?.realtime ||
72
+ null
73
+ });
67
74
  const saveSuccessOptions = normalizeSaveSuccessOptions(saveSuccess);
68
75
  const defaultFieldErrorKeys = normalizedFields.map((field) => field.key);
69
76
  const providedFieldErrorKeys = normalizeFieldErrorKeys(normalizedAddEditOptions.fieldErrorKeys);
@@ -185,6 +192,7 @@ function useCrudAddEdit({
185
192
  input: resolvedInput,
186
193
  buildRawPayload: resolveBuildRawPayload,
187
194
  mapLoadedToModel: effectiveMapLoadedToModel,
195
+ realtime: resolvedRealtime,
188
196
  onSaveSuccess: handleSaveSuccess
189
197
  });
190
198
  addEditRuntime = addEdit;
@@ -12,6 +12,7 @@ import {
12
12
  toRouteParamValue
13
13
  } from "../support/routeTemplateHelpers.js";
14
14
  import { asPlainObject } from "../support/scopeHelpers.js";
15
+ import { resolveOperationRealtimeOptions } from "../useRealtimeQueryInvalidation.js";
15
16
  import { useList } from "./useList.js";
16
17
 
17
18
  function resolveRequestQueryParamsInput(requestQueryParams, context = {}) {
@@ -49,6 +50,7 @@ function useCrudList({
49
50
  parentBinding = null,
50
51
  recordIdParam = "recordId",
51
52
  route = null,
53
+ realtime = undefined,
52
54
  ...listOptions
53
55
  } = {}) {
54
56
  const sourceRoute = route && typeof route === "object" ? route : useRoute();
@@ -72,6 +74,10 @@ function useCrudList({
72
74
  transport: resolveCrudJsonApiTransport(listOptions.transport, resource, {
73
75
  mode: "list"
74
76
  }),
77
+ realtime: resolveOperationRealtimeOptions({
78
+ realtime,
79
+ fallbackRealtime: resource?.operations?.list?.realtime || null
80
+ }),
75
81
  recordIdParam,
76
82
  requestQueryParams(context = {}) {
77
83
  const baseRequestQueryParams = resolveRequestQueryParamsInput(requestQueryParams, context);
@@ -29,6 +29,7 @@ function useCrudView({
29
29
  });
30
30
  const view = useView({
31
31
  ...viewOptions,
32
+ resource,
32
33
  transport: resolveCrudJsonApiTransport(viewOptions.transport, resource, {
33
34
  mode: "view"
34
35
  }),
@@ -8,6 +8,7 @@ import { setupOperationErrorReporting } from "../runtime/operationUiHelpers.js";
8
8
  import { createViewUiRuntime } from "../runtime/viewUiRuntime.js";
9
9
  import { createRequestQueryRuntime } from "../support/requestQueryRuntimeSupport.js";
10
10
  import { resolveRouteParamNamesInOrder } from "../support/routeTemplateHelpers.js";
11
+ import { resolveOperationRealtimeOptions } from "../useRealtimeQueryInvalidation.js";
11
12
 
12
13
  function useView({
13
14
  ownershipFilter = ROUTE_VISIBILITY_WORKSPACE,
@@ -33,7 +34,7 @@ function useView({
33
34
  listUrlTemplate = "",
34
35
  editUrlTemplate = "",
35
36
  includeRecordIdInQueryKey = false,
36
- realtime = null,
37
+ realtime = undefined,
37
38
  adapter = null
38
39
  } = {}) {
39
40
  const route = useRoute();
@@ -63,7 +64,12 @@ function useView({
63
64
  permissionSets: {
64
65
  view: viewPermissions
65
66
  },
66
- realtime
67
+ realtime: resolveOperationRealtimeOptions({
68
+ realtime,
69
+ fallbackRealtime: resource?.operations?.view?.realtime ||
70
+ resource?.operations?.list?.realtime ||
71
+ null
72
+ })
67
73
  });
68
74
  const queryParamsContext = computed(() => {
69
75
  return Object.freeze({
@@ -4,6 +4,7 @@ import { usersWebHttpClient } from "../../lib/httpClient.js";
4
4
  import { asPlainObject } from "../support/scopeHelpers.js";
5
5
  import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js";
6
6
  import { toQueryErrorMessage } from "../support/errorMessageHelpers.js";
7
+ import { useOperationRealtime } from "../useRealtimeQueryInvalidation.js";
7
8
  import {
8
9
  hasResolvedQueryData,
9
10
  mergeQueryMeta
@@ -71,6 +72,7 @@ function useEndpointResource({
71
72
  readQuery = null,
72
73
  transport = null,
73
74
  refreshOnPull = false,
75
+ realtime = null,
74
76
  queryOptions = null,
75
77
  mutationOptions = null,
76
78
  fallbackLoadError = "Unable to load resource.",
@@ -108,6 +110,11 @@ function useEndpointResource({
108
110
  })
109
111
  : asPlainObject(queryOptions))
110
112
  });
113
+ const realtimeBinding = useOperationRealtime({
114
+ realtime,
115
+ queryKey,
116
+ enabled: queryEnabled
117
+ });
111
118
 
112
119
  const mutation = useMutation({
113
120
  mutationFn: async (request = {}) => {
@@ -168,6 +175,7 @@ function useEndpointResource({
168
175
  isSaving,
169
176
  loadError,
170
177
  saveError,
178
+ realtime: realtimeBinding,
171
179
  reload,
172
180
  save
173
181
  });
@@ -14,6 +14,31 @@ function normalizeRealtimeOptions(value = {}) {
14
14
  return value;
15
15
  }
16
16
 
17
+ function hasRealtimeEventConfig(value = {}) {
18
+ const source = normalizeRealtimeOptions(value);
19
+ return Object.hasOwn(source, "event") || Object.hasOwn(source, "events");
20
+ }
21
+
22
+ function resolveOperationRealtimeOptions({
23
+ realtime = undefined,
24
+ fallbackRealtime = null
25
+ } = {}) {
26
+ if (realtime === false) {
27
+ return null;
28
+ }
29
+
30
+ const fallback = normalizeRealtimeOptions(fallbackRealtime);
31
+ const explicit = realtime == null ? {} : normalizeRealtimeOptions(realtime);
32
+ if (hasRealtimeEventConfig(explicit) || !hasRealtimeEventConfig(fallback)) {
33
+ return Object.keys(explicit).length > 0 ? explicit : null;
34
+ }
35
+
36
+ return Object.freeze({
37
+ ...fallback,
38
+ ...explicit
39
+ });
40
+ }
41
+
17
42
  function resolveEnabled(value) {
18
43
  if (typeof value === "undefined") {
19
44
  return true;
@@ -125,6 +150,7 @@ function useOperationRealtime({
125
150
  }
126
151
 
127
152
  export {
153
+ resolveOperationRealtimeOptions,
128
154
  useRealtimeQueryInvalidation,
129
155
  useOperationRealtime
130
156
  };
@@ -1,11 +1,18 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
+ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
4
+ import { createSSRApp, h } from "vue";
5
+ import { renderToString } from "vue/server-renderer";
3
6
 
4
7
  import {
5
8
  buildEndpointReadRequestOptions,
6
- buildEndpointWriteRequestOptions
9
+ buildEndpointWriteRequestOptions,
10
+ useEndpointResource
7
11
  } from "../src/client/composables/runtime/useEndpointResource.js";
8
12
  import { buildListRequestOptions } from "../src/client/composables/runtime/useListCore.js";
13
+ import {
14
+ resolveOperationRealtimeOptions
15
+ } from "../src/client/composables/useRealtimeQueryInvalidation.js";
9
16
 
10
17
  test("endpoint read request options include transport only when provided", () => {
11
18
  assert.deepEqual(
@@ -140,3 +147,105 @@ test("list request options preserve explicit limit values", () => {
140
147
  }
141
148
  );
142
149
  });
150
+
151
+ test("operation realtime options use fallback events unless explicitly overridden", () => {
152
+ const fallbackRealtime = {
153
+ events: ["contacts.record.changed"]
154
+ };
155
+
156
+ assert.deepEqual(
157
+ resolveOperationRealtimeOptions({
158
+ fallbackRealtime
159
+ }),
160
+ {
161
+ events: ["contacts.record.changed"]
162
+ }
163
+ );
164
+
165
+ assert.deepEqual(
166
+ resolveOperationRealtimeOptions({
167
+ realtime: {
168
+ enabled: false
169
+ },
170
+ fallbackRealtime
171
+ }),
172
+ {
173
+ events: ["contacts.record.changed"],
174
+ enabled: false
175
+ }
176
+ );
177
+
178
+ assert.deepEqual(
179
+ resolveOperationRealtimeOptions({
180
+ realtime: {
181
+ event: "contacts.custom.changed"
182
+ },
183
+ fallbackRealtime
184
+ }),
185
+ {
186
+ event: "contacts.custom.changed"
187
+ }
188
+ );
189
+
190
+ assert.equal(
191
+ resolveOperationRealtimeOptions({
192
+ realtime: false,
193
+ fallbackRealtime
194
+ }),
195
+ null
196
+ );
197
+ });
198
+
199
+ test("endpoint resources invalidate their query key from configured realtime events", async () => {
200
+ const queryClient = new QueryClient();
201
+ const invalidations = [];
202
+ const socketHandlers = new Map();
203
+ const socket = {
204
+ on(eventName, handler) {
205
+ socketHandlers.set(eventName, handler);
206
+ },
207
+ off(eventName, handler) {
208
+ if (socketHandlers.get(eventName) === handler) {
209
+ socketHandlers.delete(eventName);
210
+ }
211
+ }
212
+ };
213
+ queryClient.invalidateQueries = async (options = {}) => {
214
+ invalidations.push(options);
215
+ };
216
+ const app = createSSRApp({
217
+ setup() {
218
+ useEndpointResource({
219
+ queryKey: ["today-workout-detail", "/api/today/workouts/2026-05-06"],
220
+ path: "/api/today/workouts/2026-05-06",
221
+ client: {
222
+ async request() {
223
+ return {};
224
+ }
225
+ },
226
+ realtime: {
227
+ event: "workout_set_logs.record.changed"
228
+ }
229
+ });
230
+ return () => h("div");
231
+ }
232
+ });
233
+ app.use(VueQueryPlugin, {
234
+ queryClient
235
+ });
236
+ app.provide("jskit.realtime.runtime.client.socket", socket);
237
+ await renderToString(app);
238
+
239
+ const handler = socketHandlers.get("workout_set_logs.record.changed");
240
+ assert.equal(typeof handler, "function");
241
+ handler({
242
+ entityId: "42"
243
+ });
244
+ await new Promise((resolve) => setImmediate(resolve));
245
+
246
+ assert.deepEqual(invalidations, [
247
+ {
248
+ queryKey: ["today-workout-detail", "/api/today/workouts/2026-05-06"]
249
+ }
250
+ ]);
251
+ });