@lunora/vue 1.0.0-alpha.32 → 1.0.0-alpha.34

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/index.mjs CHANGED
@@ -3,11 +3,11 @@ export { hydratePreloaded } from './packem_shared/hydratePreloaded-Cnd2GmG1.mjs'
3
3
  export { LUNORA_INJECTION_KEY, createLunora, provideLunora, useLunora } from './packem_shared/LUNORA_INJECTION_KEY-DQM5MgsC.mjs';
4
4
  export { useAuth } from './packem_shared/useAuth-yXaaF-ro.mjs';
5
5
  export { default as useConnectionStatus } from './packem_shared/useConnectionStatus-DM_TOaW5.mjs';
6
- export { useFlag, useFlags } from './packem_shared/useFlag-Diuk30zd.mjs';
6
+ export { useFlag, useFlags } from './packem_shared/useFlag-Cc8rtueb.mjs';
7
7
  export { useMutation } from './packem_shared/useMutation-BKIwT8Jk.mjs';
8
8
  export { useMutator } from './packem_shared/useMutator-B_Ahs_ZN.mjs';
9
- export { useInfiniteQuery, usePaginatedQuery } from './packem_shared/useInfiniteQuery-DZMfVEuL.mjs';
10
- export { usePresence } from './packem_shared/usePresence-VXq5yGzE.mjs';
9
+ export { useInfiniteQuery, usePaginatedQuery } from './packem_shared/useInfiniteQuery-ZnvrirrF.mjs';
10
+ export { usePresence } from './packem_shared/usePresence-Bg671Pt0.mjs';
11
11
  export { subscribeToQuery, useQuery } from './packem_shared/subscribeToQuery-hVYyvgOD.mjs';
12
12
  export { useRateLimit } from './packem_shared/useRateLimit-DlPWjcX1.mjs';
13
- export { useSubscription } from './packem_shared/useSubscription-DHoCEXNh.mjs';
13
+ export { useSubscription } from './packem_shared/useSubscription-B2s-lc5e.mjs';
@@ -0,0 +1,40 @@
1
+ const compareKeys = (a, b) => {
2
+ if (a < b) {
3
+ return -1;
4
+ }
5
+ return a > b ? 1 : 0;
6
+ };
7
+ const stableStringify = (value) => {
8
+ if (value === void 0) {
9
+ return "null";
10
+ }
11
+ if (typeof value === "bigint") {
12
+ throw new TypeError("stableStringify: cannot use a bigint in a cache key (query/subscription/shape args) — pass it as a string");
13
+ }
14
+ if (value === null || typeof value !== "object") {
15
+ return JSON.stringify(value);
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
19
+ }
20
+ const proto = Object.getPrototypeOf(value);
21
+ if (proto !== null && proto !== Object.prototype) {
22
+ const name = value.constructor?.name ?? "value";
23
+ throw new TypeError(
24
+ `stableStringify: cannot use a ${name} in a cache key (query/subscription/shape args) — only plain objects, arrays, and JSON primitives are supported`
25
+ );
26
+ }
27
+ const record = value;
28
+ const keys = Object.keys(record).toSorted(compareKeys);
29
+ const parts = [];
30
+ for (const key of keys) {
31
+ const raw = record[key];
32
+ if (raw === void 0) {
33
+ continue;
34
+ }
35
+ parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
36
+ }
37
+ return `{${parts.join(",")}}`;
38
+ };
39
+
40
+ export { stableStringify as s };
@@ -1,45 +1,7 @@
1
1
  import { shallowRef, watch, toValue } from 'vue';
2
+ import { s as stableStringify } from './stable-key-wv6eP48B.mjs';
2
3
  import { useLunora } from './LUNORA_INJECTION_KEY-DQM5MgsC.mjs';
3
4
 
4
- const compareKeys = (a, b) => {
5
- if (a < b) {
6
- return -1;
7
- }
8
- return a > b ? 1 : 0;
9
- };
10
- const stableStringify = (value) => {
11
- if (value === void 0) {
12
- return "null";
13
- }
14
- if (typeof value === "bigint") {
15
- throw new TypeError("stableStringify: cannot use a bigint in a cache key (query/subscription/shape args) — pass it as a string");
16
- }
17
- if (value === null || typeof value !== "object") {
18
- return JSON.stringify(value);
19
- }
20
- if (Array.isArray(value)) {
21
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
22
- }
23
- const proto = Object.getPrototypeOf(value);
24
- if (proto !== null && proto !== Object.prototype) {
25
- const name = value.constructor?.name ?? "value";
26
- throw new TypeError(
27
- `stableStringify: cannot use a ${name} in a cache key (query/subscription/shape args) — only plain objects, arrays, and JSON primitives are supported`
28
- );
29
- }
30
- const record = value;
31
- const keys = Object.keys(record).toSorted(compareKeys);
32
- const parts = [];
33
- for (const key of keys) {
34
- const raw = record[key];
35
- if (raw === void 0) {
36
- continue;
37
- }
38
- parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
39
- }
40
- return `{${parts.join(",")}}`;
41
- };
42
-
43
5
  const FLAGS_EVAL_PATH = "__lunora_flags__:eval";
44
6
  const flagKind = (value) => {
45
7
  const kind = typeof value;
@@ -1,8 +1,9 @@
1
1
  import { shallowRef, watch, toValue, onScopeDispose, computed } from 'vue';
2
2
  import { initialPages, derivePaginationStatus, rebalance, applyLoadMore } from '@lunora/client/pagination';
3
+ import { s as stableStringify } from './stable-key-wv6eP48B.mjs';
3
4
  import { useLunora } from './LUNORA_INJECTION_KEY-DQM5MgsC.mjs';
4
5
 
5
- const buildPageKey = (functionPath, pageArgs) => `${functionPath}::${JSON.stringify(pageArgs)}`;
6
+ const buildPageKey = (functionPath, pageArgs) => `${functionPath}::${stableStringify(pageArgs)}`;
6
7
  const buildPageArgs = (page, baseArgs) => {
7
8
  return {
8
9
  ...baseArgs,
@@ -14,6 +15,7 @@ const usePaginatedCore = (function_, args, options) => {
14
15
  const { initialNumItems, shardKey } = options;
15
16
  const pages = shallowRef(initialPages(initialNumItems));
16
17
  const pageResults = shallowRef([]);
18
+ let lastLoadMoreCursor;
17
19
  const activeSubs = /* @__PURE__ */ new Map();
18
20
  const resultsByKey = /* @__PURE__ */ new Map();
19
21
  const pendingPageKeys = /* @__PURE__ */ new Set();
@@ -49,6 +51,7 @@ const usePaginatedCore = (function_, args, options) => {
49
51
  entry.unsub();
50
52
  activeSubs.delete(originalKey);
51
53
  resultsByKey.delete(entry.currentKey);
54
+ pendingPageKeys.delete(entry.currentKey);
52
55
  }
53
56
  }
54
57
  const coveredKeys = new Set([...activeSubs.values()].map((subEntry) => subEntry.currentKey));
@@ -93,13 +96,16 @@ const usePaginatedCore = (function_, args, options) => {
93
96
  }
94
97
  activeSubs.clear();
95
98
  resultsByKey.clear();
99
+ pendingPageKeys.clear();
96
100
  };
97
101
  watch(
98
- () => toValue(args),
99
- (current) => {
102
+ () => stableStringify(toValue(args)),
103
+ () => {
104
+ const current = toValue(args);
100
105
  teardownAll();
101
106
  pages.value = initialPages(initialNumItems);
102
107
  pageResults.value = [];
108
+ lastLoadMoreCursor = void 0;
103
109
  if (current !== "skip") {
104
110
  syncSubscriptions(pages.value, current);
105
111
  rebuildPageResults(pages.value, current);
@@ -135,10 +141,14 @@ const usePaginatedCore = (function_, args, options) => {
135
141
  if (currentStatus !== "CanLoadMore") {
136
142
  return;
137
143
  }
144
+ if (nextCursor === lastLoadMoreCursor) {
145
+ return;
146
+ }
138
147
  const next = applyLoadMore(pages.value, nextCursor, numberItems);
139
148
  if (!next) {
140
149
  return;
141
150
  }
151
+ lastLoadMoreCursor = nextCursor;
142
152
  const oldTail = pages.value.at(-1);
143
153
  const newPinnedPage = next.at(-2);
144
154
  if (oldTail && newPinnedPage) {
@@ -0,0 +1,75 @@
1
+ import { shallowRef, getCurrentScope, onScopeDispose } from 'vue';
2
+ import { useLunora } from './LUNORA_INJECTION_KEY-DQM5MgsC.mjs';
3
+
4
+ const randomSessionId = (prefix = "sess") => {
5
+ if (typeof crypto !== "undefined") {
6
+ if (typeof crypto.randomUUID === "function") {
7
+ return crypto.randomUUID();
8
+ }
9
+ if (typeof crypto.getRandomValues === "function") {
10
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
11
+ return `${prefix}-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
12
+ }
13
+ }
14
+ return `${prefix}-${Date.now().toString(36)}`;
15
+ };
16
+
17
+ const DEFAULT_INTERVAL_MS = 1e4;
18
+ const usePresence = (roomId, options) => {
19
+ const client = useLunora();
20
+ const { heartbeat, intervalMs = DEFAULT_INTERVAL_MS, listPresent, shardKey } = options;
21
+ const sessionId = options.sessionId ?? randomSessionId();
22
+ const present = shallowRef(void 0);
23
+ let latestData = options.data;
24
+ const sendHeartbeat = () => {
25
+ const args = {
26
+ roomId,
27
+ sessionId,
28
+ ...latestData === void 0 ? {} : { data: latestData }
29
+ };
30
+ client.mutation(heartbeat, args, { shardKey }).catch(() => void 0);
31
+ };
32
+ const setData = (next) => {
33
+ latestData = next;
34
+ sendHeartbeat();
35
+ };
36
+ if (globalThis.window !== void 0) {
37
+ sendHeartbeat();
38
+ const intervalHandle = setInterval(sendHeartbeat, intervalMs);
39
+ const onVisible = () => {
40
+ if (typeof document !== "undefined" && document.visibilityState === "visible") {
41
+ sendHeartbeat();
42
+ }
43
+ };
44
+ if (typeof document !== "undefined") {
45
+ document.addEventListener("visibilitychange", onVisible);
46
+ }
47
+ const releaseConnectionContext = client.acquireConnectionContext({ roomId, sessionId }, { shardKey });
48
+ const unsubscribe = client.subscribe(
49
+ listPresent,
50
+ { roomId },
51
+ (value) => {
52
+ present.value = value;
53
+ },
54
+ { shardKey }
55
+ );
56
+ const teardown = () => {
57
+ clearInterval(intervalHandle);
58
+ if (typeof document !== "undefined") {
59
+ document.removeEventListener("visibilitychange", onVisible);
60
+ }
61
+ releaseConnectionContext();
62
+ unsubscribe();
63
+ };
64
+ if (getCurrentScope()) {
65
+ onScopeDispose(teardown);
66
+ } else {
67
+ console.warn(
68
+ "[@lunora/vue] usePresence called with no active effect scope — its heartbeat interval and live subscription will not be cleaned up automatically. Call it inside setup()/an effect scope."
69
+ );
70
+ }
71
+ }
72
+ return { present, sessionId, setData };
73
+ };
74
+
75
+ export { usePresence };
@@ -1,4 +1,5 @@
1
1
  import { createQuerySubscription } from '@lunora/client/query';
2
+ import { LunoraError } from '@lunora/errors';
2
3
  import { ref, watch, toValue, onScopeDispose } from 'vue';
3
4
  import { useLunora } from './LUNORA_INJECTION_KEY-DQM5MgsC.mjs';
4
5
 
@@ -24,7 +25,7 @@ const useSubscription = (function_, args, options = {}) => {
24
25
  error.value = void 0;
25
26
  },
26
27
  onError: (subscriptionError) => {
27
- error.value = new Error(subscriptionError.message);
28
+ error.value = subscriptionError.code === void 0 ? new Error(subscriptionError.message) : new LunoraError(subscriptionError.code, subscriptionError.message);
28
29
  data.value = void 0;
29
30
  },
30
31
  onReset: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/vue",
3
- "version": "1.0.0-alpha.32",
3
+ "version": "1.0.0-alpha.34",
4
4
  "description": "Vue adapter for Lunora — live composables, optimistic mutations, and reactive loaders",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -54,10 +54,10 @@
54
54
  "access": "public"
55
55
  },
56
56
  "dependencies": {
57
- "@lunora/client": "1.0.0-alpha.20",
58
- "@lunora/errors": "1.0.0-alpha.3",
59
- "@lunora/ratelimit": "1.0.0-alpha.6",
60
- "@lunora/runtime": "1.0.0-alpha.22"
57
+ "@lunora/client": "1.0.0-alpha.21",
58
+ "@lunora/errors": "1.0.0-alpha.4",
59
+ "@lunora/ratelimit": "1.0.0-alpha.7",
60
+ "@lunora/runtime": "1.0.0-alpha.24"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "vue": "^3.5.0"
@@ -1,59 +0,0 @@
1
- import { shallowRef, onScopeDispose } from 'vue';
2
- import { useLunora } from './LUNORA_INJECTION_KEY-DQM5MgsC.mjs';
3
-
4
- const makeSessionId = () => {
5
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
6
- return crypto.randomUUID();
7
- }
8
- return `sess-${Math.random().toString(36).slice(2)}-${String(Date.now())}`;
9
- };
10
- const DEFAULT_INTERVAL_MS = 1e4;
11
- const usePresence = (roomId, options) => {
12
- const client = useLunora();
13
- const { heartbeat, intervalMs = DEFAULT_INTERVAL_MS, listPresent, shardKey } = options;
14
- const sessionId = options.sessionId ?? makeSessionId();
15
- const present = shallowRef(void 0);
16
- let latestData = options.data;
17
- const sendHeartbeat = () => {
18
- const args = {
19
- roomId,
20
- sessionId,
21
- ...latestData === void 0 ? {} : { data: latestData }
22
- };
23
- client.mutation(heartbeat, args, { shardKey }).catch(() => void 0);
24
- };
25
- const setData = (next) => {
26
- latestData = next;
27
- sendHeartbeat();
28
- };
29
- sendHeartbeat();
30
- const intervalHandle = setInterval(sendHeartbeat, intervalMs);
31
- const onVisible = () => {
32
- if (typeof document !== "undefined" && document.visibilityState === "visible") {
33
- sendHeartbeat();
34
- }
35
- };
36
- if (typeof document !== "undefined") {
37
- document.addEventListener("visibilitychange", onVisible);
38
- }
39
- const releaseConnectionContext = client.acquireConnectionContext({ roomId, sessionId }, { shardKey });
40
- const unsubscribe = client.subscribe(
41
- listPresent,
42
- { roomId },
43
- (value) => {
44
- present.value = value;
45
- },
46
- { shardKey }
47
- );
48
- onScopeDispose(() => {
49
- clearInterval(intervalHandle);
50
- if (typeof document !== "undefined") {
51
- document.removeEventListener("visibilitychange", onVisible);
52
- }
53
- releaseConnectionContext();
54
- unsubscribe();
55
- });
56
- return { present, sessionId, setData };
57
- };
58
-
59
- export { usePresence };