@graffiti-garden/wrapper-vue 0.7.2 → 1.0.4

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.
Files changed (70) hide show
  1. package/README.md +2 -3
  2. package/dist/browser/ajv-D_HICdxS.mjs +4447 -0
  3. package/dist/browser/ajv-D_HICdxS.mjs.map +1 -0
  4. package/dist/browser/plugin.mjs +1003 -943
  5. package/dist/browser/plugin.mjs.map +1 -1
  6. package/dist/node/components/ActorToHandle.vue.d.ts +23 -0
  7. package/dist/node/components/ActorToHandle.vue.d.ts.map +1 -0
  8. package/dist/node/{Discover.vue.d.ts → components/Discover.vue.d.ts} +4 -4
  9. package/dist/node/components/Discover.vue.d.ts.map +1 -0
  10. package/dist/node/{Get.vue.d.ts → components/Get.vue.d.ts} +2 -5
  11. package/dist/node/components/Get.vue.d.ts.map +1 -0
  12. package/dist/node/components/GetMedia.vue.d.ts +36 -0
  13. package/dist/node/components/GetMedia.vue.d.ts.map +1 -0
  14. package/dist/node/components/HandleToActor.vue.d.ts +23 -0
  15. package/dist/node/components/HandleToActor.vue.d.ts.map +1 -0
  16. package/dist/node/components/ObjectInfo.vue.d.ts +7 -0
  17. package/dist/node/components/ObjectInfo.vue.d.ts.map +1 -0
  18. package/dist/node/composables/actor-to-handle.d.ts +25 -0
  19. package/dist/node/composables/actor-to-handle.d.ts.map +1 -0
  20. package/dist/node/composables/discover.d.ts +38 -0
  21. package/dist/node/composables/discover.d.ts.map +1 -0
  22. package/dist/node/composables/get-media.d.ts +31 -0
  23. package/dist/node/composables/get-media.d.ts.map +1 -0
  24. package/dist/node/composables/get.d.ts +28 -0
  25. package/dist/node/composables/get.d.ts.map +1 -0
  26. package/dist/node/composables/handle-to-actor.d.ts +25 -0
  27. package/dist/node/composables/handle-to-actor.d.ts.map +1 -0
  28. package/dist/node/composables/resolve-string.d.ts +6 -0
  29. package/dist/node/composables/resolve-string.d.ts.map +1 -0
  30. package/dist/node/globals.d.ts +3 -5
  31. package/dist/node/globals.d.ts.map +1 -1
  32. package/dist/node/plugin.d.ts +174 -75
  33. package/dist/node/plugin.d.ts.map +1 -1
  34. package/dist/node/plugin.js +1 -1
  35. package/dist/node/plugin.js.map +1 -1
  36. package/dist/node/plugin.mjs +468 -333
  37. package/dist/node/plugin.mjs.map +1 -1
  38. package/package.json +15 -14
  39. package/src/components/ActorToHandle.vue +16 -0
  40. package/src/{Discover.vue → components/Discover.vue} +15 -9
  41. package/src/{Get.vue → components/Get.vue} +7 -11
  42. package/src/components/GetMedia.vue +75 -0
  43. package/src/components/HandleToActor.vue +16 -0
  44. package/src/components/ObjectInfo.vue +127 -0
  45. package/src/composables/actor-to-handle.ts +32 -0
  46. package/src/composables/discover.ts +202 -0
  47. package/src/composables/get-media.ts +116 -0
  48. package/src/composables/get.ts +109 -0
  49. package/src/composables/handle-to-actor.ts +32 -0
  50. package/src/composables/resolve-string.ts +46 -0
  51. package/src/globals.ts +24 -2
  52. package/src/plugin.ts +84 -29
  53. package/dist/browser/ajv-C30pimY5.mjs +0 -4400
  54. package/dist/browser/ajv-C30pimY5.mjs.map +0 -1
  55. package/dist/browser/index-CWfNKdDL.mjs +0 -424
  56. package/dist/browser/index-CWfNKdDL.mjs.map +0 -1
  57. package/dist/node/Discover.vue.d.ts.map +0 -1
  58. package/dist/node/Get.vue.d.ts.map +0 -1
  59. package/dist/node/RecoverOrphans.vue.d.ts +0 -31
  60. package/dist/node/RecoverOrphans.vue.d.ts.map +0 -1
  61. package/dist/node/composables.d.ts +0 -75
  62. package/dist/node/composables.d.ts.map +0 -1
  63. package/dist/node/pollers.d.ts +0 -28
  64. package/dist/node/pollers.d.ts.map +0 -1
  65. package/dist/node/reducers.d.ts +0 -37
  66. package/dist/node/reducers.d.ts.map +0 -1
  67. package/src/RecoverOrphans.vue +0 -37
  68. package/src/composables.ts +0 -347
  69. package/src/pollers.ts +0 -119
  70. package/src/reducers.ts +0 -124
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import type { GraffitiObjectBase, GraffitiSession } from "@graffiti-garden/api";
3
+ import ActorToHandle from "./ActorToHandle.vue";
4
+ import { useGraffiti } from "../globals";
5
+ import { ref } from "vue";
6
+
7
+ defineProps<{
8
+ object: GraffitiObjectBase | null | undefined;
9
+ }>();
10
+
11
+ const graffiti = useGraffiti();
12
+ const deleting = ref(false);
13
+ async function deleteObject(
14
+ object: GraffitiObjectBase,
15
+ session: GraffitiSession,
16
+ ) {
17
+ deleting.value = true;
18
+ await new Promise((resolve) => setTimeout(resolve, 0));
19
+ if (
20
+ confirm(
21
+ "Are you sure you want to delete this object? It cannot be undone.",
22
+ )
23
+ ) {
24
+ await graffiti.delete(object, session);
25
+ }
26
+ deleting.value = false;
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <article v-if="object" :data-url="object.url">
32
+ <header>
33
+ <h2>Graffiti Object</h2>
34
+
35
+ <dl>
36
+ <dt>Object URL</dt>
37
+ <dd>
38
+ <code>{{ object.url }}</code>
39
+ </dd>
40
+
41
+ <dt>Actor</dt>
42
+ <dd>
43
+ <code>{{ object.actor }}</code>
44
+ </dd>
45
+
46
+ <dt>Handle</dt>
47
+ <dd>
48
+ <ActorToHandle :actor="object.actor" />
49
+ </dd>
50
+ </dl>
51
+ </header>
52
+
53
+ <section>
54
+ <h3>Content</h3>
55
+ <pre>{{ object.value }}</pre>
56
+ </section>
57
+
58
+ <section>
59
+ <h3>Allowed Actors</h3>
60
+
61
+ <p v-if="!Array.isArray(object.allowed)">
62
+ <em>Public</em>
63
+ </p>
64
+ <p v-else-if="object.allowed.length === 0">
65
+ <em>Noone</em>
66
+ </p>
67
+ <ul>
68
+ <li v-for="actor in object.allowed" :key="actor">
69
+ <dl>
70
+ <dt>Actor</dt>
71
+ <dd>
72
+ <code>{{ actor }}</code>
73
+ </dd>
74
+ <dt>Handle</dt>
75
+ <dd>
76
+ <ActorToHandle :actor="actor" />
77
+ </dd>
78
+ </dl>
79
+ </li>
80
+ </ul>
81
+ </section>
82
+
83
+ <section>
84
+ <h3>Channels</h3>
85
+
86
+ <ul v-if="object.channels?.length">
87
+ <li v-for="channel in object.channels" :key="channel">
88
+ <code>{{ channel }}</code>
89
+ </li>
90
+ </ul>
91
+ <p v-else>
92
+ <em>No channels</em>
93
+ </p>
94
+ </section>
95
+
96
+ <footer>
97
+ <nav>
98
+ <ul>
99
+ <li v-if="$graffitiSession.value?.actor === object.actor">
100
+ <button
101
+ :disabled="deleting"
102
+ @click="
103
+ deleteObject(object, $graffitiSession.value)
104
+ "
105
+ >
106
+ {{ deleting ? "Deleting..." : "Delete" }}
107
+ </button>
108
+ </li>
109
+ </ul>
110
+ </nav>
111
+ </footer>
112
+ </article>
113
+
114
+ <article v-else-if="object === null">
115
+ <header>
116
+ <h2>Graffiti Object</h2>
117
+ </header>
118
+ <p><em>Object not found</em></p>
119
+ </article>
120
+
121
+ <article v-else>
122
+ <header>
123
+ <h2>Graffiti Object</h2>
124
+ </header>
125
+ <p><em>Loading...</em></p>
126
+ </article>
127
+ </template>
@@ -0,0 +1,32 @@
1
+ import type { MaybeRefOrGetter } from "vue";
2
+ import { useGraffiti } from "../globals";
3
+ import { useResolveString } from "./resolve-string";
4
+
5
+ /**
6
+ * The [Graffiti.actorToHandle](https://api.graffiti.garden/classes/Graffiti.html#actortohandle)
7
+ * method as a reactive [composable](https://vuejs.org/guide/reusability/composables.html)
8
+ * for use in the Vue [composition API](https://vuejs.org/guide/introduction.html#composition-api).
9
+ *
10
+ * Its corresponding renderless component is {@link GraffitiActorToHandle}.
11
+ *
12
+ * The arguments of this composable are the same as Graffiti.actorToHandle,
13
+ * only they can also be [Refs](https://vuejs.org/api/reactivity-core.html#ref)
14
+ * or [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#description).
15
+ * As they change the output will automatically update.
16
+ * Reactivity only triggers when the root array or object changes,
17
+ * not when the elements or properties change.
18
+ * If you need deep reactivity, wrap your argument in a getter.
19
+ *
20
+ * @returns
21
+ * - `handle`: A [ref](https://vuejs.org/api/reactivity-core.html#ref) that contains
22
+ * the retrieved handle, if it exists. If the handle cannot be found, the result
23
+ * is `null`. If the handle is still being fetched, the result is `undefined`.
24
+ */
25
+ export function useGraffitiActorToHandle(actor: MaybeRefOrGetter<string>) {
26
+ const graffiti = useGraffiti();
27
+ const { output } = useResolveString(
28
+ actor,
29
+ graffiti.actorToHandle.bind(graffiti),
30
+ );
31
+ return { handle: output };
32
+ }
@@ -0,0 +1,202 @@
1
+ import type {
2
+ GraffitiObject,
3
+ GraffitiObjectStreamContinue,
4
+ GraffitiObjectStreamContinueEntry,
5
+ GraffitiObjectStreamError,
6
+ GraffitiObjectStreamReturn,
7
+ GraffitiSession,
8
+ JSONSchema,
9
+ } from "@graffiti-garden/api";
10
+ import { GraffitiErrorNotFound } from "@graffiti-garden/api";
11
+ import type { MaybeRefOrGetter, Ref } from "vue";
12
+ import { ref, toValue, watch, onScopeDispose } from "vue";
13
+ import { useGraffitiSynchronize } from "../globals";
14
+
15
+ /**
16
+ * The [Graffiti.discover](https://api.graffiti.garden/classes/Graffiti.html#discover)
17
+ * method as a reactive [composable](https://vuejs.org/guide/reusability/composables.html)
18
+ * for use in the Vue [composition API](https://vuejs.org/guide/introduction.html#composition-api).
19
+ *
20
+ * Its corresponding renderless component is {@link GraffitiDiscover}.
21
+ *
22
+ * The arguments of this composable are largely the same as Graffiti.discover,
23
+ * only they can also be [Refs](https://vuejs.org/api/reactivity-core.html#ref)
24
+ * or [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#description).
25
+ * There is one additional optional argument `autopoll`, which when `true`,
26
+ * will automatically poll for new objects.
27
+ * As they change the arguments change, the output will automatically update.
28
+ * Reactivity only triggers when the root array or object changes,
29
+ * not when the elements or properties change.
30
+ * If you need deep reactivity, wrap your argument in a getter.
31
+ *
32
+ * @returns
33
+ * - `objects`: A [ref](https://vuejs.org/api/reactivity-core.html#ref) that contains
34
+ * an array of Graffiti objects.
35
+ * - `poll`: A function that can be called to manually check for objects.
36
+ * - `isFirstPoll`: A boolean [ref](https://vuejs.org/api/reactivity-core.html#ref)
37
+ * that indicates if the *first* poll after a change of arguments is currently running.
38
+ * It may be used to show a loading spinner or disable a button, or it can be watched
39
+ * to know when the `objects` array is ready to use.
40
+ */
41
+ export function useGraffitiDiscover<Schema extends JSONSchema>(
42
+ channels: MaybeRefOrGetter<string[]>,
43
+ schema: MaybeRefOrGetter<Schema>,
44
+ session?: MaybeRefOrGetter<GraffitiSession | undefined | null>,
45
+ /**
46
+ * Whether to automatically poll for new objects.
47
+ */
48
+ autopoll: MaybeRefOrGetter<boolean> = false,
49
+ ) {
50
+ const graffiti = useGraffitiSynchronize();
51
+
52
+ // Output
53
+ const objectsRaw: Map<string, GraffitiObject<Schema>> = new Map();
54
+ const objects: Ref<GraffitiObject<Schema>[]> = ref([]);
55
+ let poll_ = async () => {};
56
+ const poll = async () => poll_();
57
+ const isFirstPoll = ref(true);
58
+
59
+ // Maintain iterators for disposal
60
+ let syncIterator: AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>>;
61
+ let discoverIterator: GraffitiObjectStreamContinue<Schema>;
62
+ onScopeDispose(() => {
63
+ syncIterator?.return(null);
64
+ discoverIterator?.return({
65
+ continue: () => discoverIterator,
66
+ cursor: "",
67
+ });
68
+ });
69
+
70
+ const refresh = ref(0);
71
+ function restartWatch(timeout = 0) {
72
+ setTimeout(() => {
73
+ refresh.value++;
74
+ }, timeout);
75
+ }
76
+ watch(
77
+ () => ({
78
+ args: [toValue(channels), toValue(schema), toValue(session)] as const,
79
+ refresh: refresh.value,
80
+ }),
81
+ ({ args }, _prev, onInvalidate) => {
82
+ // Reset the output
83
+ objectsRaw.clear();
84
+ objects.value = [];
85
+ isFirstPoll.value = true;
86
+
87
+ // Initialize new iterators
88
+ const mySyncIterator = graffiti.synchronizeDiscover<Schema>(...args);
89
+ syncIterator = mySyncIterator;
90
+ let myDiscoverIterator: GraffitiObjectStreamContinue<Schema>;
91
+
92
+ // Set up automatic iterator cleanup
93
+ let active = true;
94
+ onInvalidate(() => {
95
+ active = false;
96
+ mySyncIterator.return(null);
97
+ myDiscoverIterator?.return({
98
+ continue: () => discoverIterator,
99
+ cursor: "",
100
+ });
101
+ });
102
+
103
+ // Start to synchronize in the background
104
+ // (all polling results will go through here)
105
+ let batchFlattenPromise: Promise<void> | undefined = undefined;
106
+ (async () => {
107
+ for await (const result of mySyncIterator) {
108
+ if (!active) break;
109
+ if (result.tombstone) {
110
+ objectsRaw.delete(result.object.url);
111
+ } else {
112
+ objectsRaw.set(result.object.url, result.object);
113
+ }
114
+ // Flatten objects in batches to prevent
115
+ // excessive re-rendering
116
+ if (!batchFlattenPromise) {
117
+ batchFlattenPromise = new Promise<void>((resolve) => {
118
+ setTimeout(() => {
119
+ if (active) {
120
+ objects.value = Array.from(objectsRaw.values());
121
+ }
122
+ batchFlattenPromise = undefined;
123
+ resolve();
124
+ }, 50);
125
+ });
126
+ }
127
+ }
128
+ })();
129
+
130
+ // Then set up a polling function
131
+ let polling = false;
132
+ let continueFn: GraffitiObjectStreamReturn<Schema>["continue"] = () =>
133
+ graffiti.discover<Schema>(...args);
134
+ poll_ = async () => {
135
+ if (polling || !active) return;
136
+ polling = true;
137
+
138
+ // Try to start the iterator
139
+ try {
140
+ myDiscoverIterator = continueFn(args[2]);
141
+ } catch (e) {
142
+ // Discovery is lazy so this should not happen,
143
+ // wait a bit before retrying
144
+ return restartWatch(5000);
145
+ }
146
+ if (!active) return;
147
+ discoverIterator = myDiscoverIterator;
148
+
149
+ while (true) {
150
+ let result: IteratorResult<
151
+ | GraffitiObjectStreamContinueEntry<Schema>
152
+ | GraffitiObjectStreamError,
153
+ GraffitiObjectStreamReturn<Schema>
154
+ >;
155
+ try {
156
+ result = await myDiscoverIterator.next();
157
+ } catch (e) {
158
+ if (e instanceof GraffitiErrorNotFound) {
159
+ // The cursor has expired, we need to start from scratch.
160
+ return restartWatch();
161
+ } else {
162
+ // If something else went wrong, wait a bit before retrying
163
+ return restartWatch(5000);
164
+ }
165
+ }
166
+ if (!active) return;
167
+ if (result.done) {
168
+ continueFn = result.value.continue;
169
+ break;
170
+ } else if (result.value.error) {
171
+ // Non-fatal errors do not stop the stream
172
+ console.error(result.value.error);
173
+ }
174
+ }
175
+
176
+ // Wait for sync to receive updates
177
+ await new Promise((resolve) => setTimeout(resolve, 0));
178
+ // And wait for pending results to be flattened
179
+ if (batchFlattenPromise) await batchFlattenPromise;
180
+
181
+ if (!active) return;
182
+ polling = false;
183
+ isFirstPoll.value = false;
184
+ if (toValue(autopoll)) poll();
185
+ };
186
+ poll();
187
+ },
188
+ { immediate: true },
189
+ );
190
+
191
+ // Start polling if autopoll turns true
192
+ watch(
193
+ () => toValue(autopoll),
194
+ (value) => value && poll(),
195
+ );
196
+
197
+ return {
198
+ objects,
199
+ poll,
200
+ isFirstPoll,
201
+ };
202
+ }
@@ -0,0 +1,116 @@
1
+ import type {
2
+ GraffitiMedia,
3
+ GraffitiMediaAccept,
4
+ GraffitiSession,
5
+ } from "@graffiti-garden/api";
6
+ import { GraffitiErrorNotFound } from "@graffiti-garden/api";
7
+ import type { MaybeRefOrGetter, Ref } from "vue";
8
+ import { ref, toValue, watch, onScopeDispose } from "vue";
9
+ import { useGraffitiSynchronize } from "../globals";
10
+
11
+ /**
12
+ * The [Graffiti.getMedia](https://api.graffiti.garden/classes/Graffiti.html#getMedia)
13
+ * method as a reactive [composable](https://vuejs.org/guide/reusability/composables.html)
14
+ * for use in the Vue [composition API](https://vuejs.org/guide/introduction.html#composition-api).
15
+ *
16
+ * Its corresponding renderless component is {@link GraffitiGetMedia}.
17
+ *
18
+ * The arguments of this composable are the same as Graffiti.getMedia,
19
+ * only they can also be [Refs](https://vuejs.org/api/reactivity-core.html#ref)
20
+ * or [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#description).
21
+ * As they change the output will automatically update.
22
+ * Reactivity only triggers when the root array or object changes,
23
+ * not when the elements or properties change.
24
+ * If you need deep reactivity, wrap your argument in a getter.
25
+ *
26
+ * @returns
27
+ * - `media`: A [ref](https://vuejs.org/api/reactivity-core.html#ref) that contains
28
+ * the retrieved Graffiti media, if it exists. The media will include a `dataUrl` property
29
+ * that can be used to directly display the media in a template. If the media has been deleted,
30
+ * the result is `null`. If the media is still being fetched, the result is `undefined`.
31
+ * - `poll`: A function that can be called to manually check if the media has changed.
32
+ */
33
+ export function useGraffitiGetMedia(
34
+ url: MaybeRefOrGetter<string>,
35
+ accept: MaybeRefOrGetter<GraffitiMediaAccept>,
36
+ session?: MaybeRefOrGetter<GraffitiSession | undefined | null>,
37
+ ): {
38
+ media: Ref<(GraffitiMedia & { dataUrl: string }) | null | undefined>;
39
+ poll: () => Promise<void>;
40
+ } {
41
+ const graffiti = useGraffitiSynchronize();
42
+ const media = ref<(GraffitiMedia & { dataUrl: string }) | null | undefined>(
43
+ undefined,
44
+ );
45
+
46
+ // The "poll counter" is a hack to get
47
+ // watch to refresh
48
+ const pollCounter = ref(0);
49
+ let pollPromise: Promise<void> | null = null;
50
+ let resolvePoll = () => {};
51
+ function poll() {
52
+ if (pollPromise) return pollPromise;
53
+ pollCounter.value++;
54
+ // Wait until the watch finishes and calls
55
+ // "pollResolve" to finish the poll
56
+ pollPromise = new Promise<void>((resolve) => {
57
+ resolvePoll = () => {
58
+ pollPromise = null;
59
+ resolve();
60
+ };
61
+ });
62
+ return pollPromise;
63
+ }
64
+ watch(
65
+ () => ({
66
+ args: [toValue(url), toValue(accept), toValue(session)] as const,
67
+ pollCounter: pollCounter.value,
68
+ }),
69
+ async ({ args }, _prev, onInvalidate) => {
70
+ // Revoke the data URL to prevent a memory leak
71
+ if (media.value?.dataUrl) {
72
+ URL.revokeObjectURL(media.value.dataUrl);
73
+ }
74
+ media.value = undefined;
75
+
76
+ let active = true;
77
+ onInvalidate(() => {
78
+ active = false;
79
+ });
80
+
81
+ try {
82
+ const { data, actor, allowed } = await graffiti.getMedia(...args);
83
+ if (!active) return;
84
+ const dataUrl = URL.createObjectURL(data);
85
+ media.value = {
86
+ data,
87
+ dataUrl,
88
+ actor,
89
+ allowed,
90
+ };
91
+ } catch (e) {
92
+ if (!active) return;
93
+ if (e instanceof GraffitiErrorNotFound) {
94
+ media.value = null;
95
+ } else {
96
+ console.error(e);
97
+ }
98
+ } finally {
99
+ resolvePoll();
100
+ }
101
+ },
102
+ { immediate: true },
103
+ );
104
+
105
+ onScopeDispose(() => {
106
+ resolvePoll();
107
+ if (media.value?.dataUrl) {
108
+ URL.revokeObjectURL(media.value.dataUrl);
109
+ }
110
+ });
111
+
112
+ return {
113
+ media,
114
+ poll,
115
+ };
116
+ }
@@ -0,0 +1,109 @@
1
+ import type {
2
+ GraffitiObject,
3
+ GraffitiObjectUrl,
4
+ GraffitiObjectStreamContinueEntry,
5
+ GraffitiSession,
6
+ JSONSchema,
7
+ } from "@graffiti-garden/api";
8
+ import { GraffitiErrorNotFound } from "@graffiti-garden/api";
9
+ import type { MaybeRefOrGetter, Ref } from "vue";
10
+ import { ref, toValue, watch, onScopeDispose } from "vue";
11
+ import { useGraffitiSynchronize } from "../globals";
12
+
13
+ /**
14
+ * The [Graffiti.get](https://api.graffiti.garden/classes/Graffiti.html#get)
15
+ * method as a reactive [composable](https://vuejs.org/guide/reusability/composables.html)
16
+ * for use in the Vue [composition API](https://vuejs.org/guide/introduction.html#composition-api).
17
+ *
18
+ * Its corresponding renderless component is {@link GraffitiGet}.
19
+ *
20
+ * The arguments of this composable are the same as Graffiti.get,
21
+ * only they can also be [Refs](https://vuejs.org/api/reactivity-core.html#ref)
22
+ * or [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#description).
23
+ * As they change the output will automatically update.
24
+ * Reactivity only triggers when the root array or object changes,
25
+ * not when the elements or properties change.
26
+ * If you need deep reactivity, wrap your argument in a getter.
27
+ *
28
+ * @returns
29
+ * - `object`: A [ref](https://vuejs.org/api/reactivity-core.html#ref) that contains
30
+ * the retrieved Graffiti object, if it exists. If the object cannot be found,
31
+ * the result is `null`. If the object is still being fetched, the result is `undefined`.
32
+ * - `poll`: A function that can be called to manually check if the object has changed.
33
+ */
34
+ export function useGraffitiGet<Schema extends JSONSchema>(
35
+ url: MaybeRefOrGetter<GraffitiObjectUrl | string>,
36
+ schema: MaybeRefOrGetter<Schema>,
37
+ session?: MaybeRefOrGetter<GraffitiSession | undefined | null>,
38
+ ): {
39
+ object: Ref<GraffitiObject<Schema> | null | undefined>;
40
+ poll: () => Promise<void>;
41
+ } {
42
+ const graffiti = useGraffitiSynchronize();
43
+
44
+ const object: Ref<GraffitiObject<Schema> | null | undefined> = ref(undefined);
45
+ let poll_ = async () => {};
46
+ const poll = async () => poll_();
47
+
48
+ let iterator: AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>>;
49
+ onScopeDispose(() => {
50
+ iterator?.return(null);
51
+ });
52
+
53
+ watch(
54
+ () => [toValue(url), toValue(schema), toValue(session)] as const,
55
+ (args, _prev, onInvalidate) => {
56
+ // Reset the object value (undefined = "loading")
57
+ object.value = undefined;
58
+
59
+ // Initialize a new iterator
60
+ const myIterator = graffiti.synchronizeGet<Schema>(...args);
61
+ iterator = myIterator;
62
+
63
+ // Make sure to dispose of the iterator when invalidated
64
+ let active = true;
65
+ onInvalidate(() => {
66
+ active = false;
67
+ myIterator.return(null);
68
+ });
69
+
70
+ // Listen to the iterator in the background,
71
+ // it will receive results from polling below
72
+ (async () => {
73
+ for await (const result of myIterator) {
74
+ if (!active) return;
75
+ if (result.tombstone) {
76
+ object.value = null;
77
+ } else {
78
+ object.value = result.object;
79
+ }
80
+ }
81
+ })();
82
+
83
+ // Then set up a polling function
84
+ let polling = false;
85
+ poll_ = async () => {
86
+ if (polling || !active) return;
87
+ polling = true;
88
+ try {
89
+ await graffiti.get<Schema>(...args);
90
+ } catch (e) {
91
+ if (!(e instanceof GraffitiErrorNotFound)) {
92
+ console.error(e);
93
+ }
94
+ }
95
+
96
+ // Wait for sync to receive the update
97
+ await new Promise((resolve) => setTimeout(resolve, 0));
98
+ polling = false;
99
+ };
100
+ poll();
101
+ },
102
+ { immediate: true },
103
+ );
104
+
105
+ return {
106
+ object,
107
+ poll,
108
+ };
109
+ }
@@ -0,0 +1,32 @@
1
+ import type { MaybeRefOrGetter } from "vue";
2
+ import { useGraffiti } from "../globals";
3
+ import { useResolveString } from "./resolve-string";
4
+
5
+ /**
6
+ * The [Graffiti.handleToActor](https://api.graffiti.garden/classes/Graffiti.html#handletoactor)
7
+ * method as a reactive [composable](https://vuejs.org/guide/reusability/composables.html)
8
+ * for use in the Vue [composition API](https://vuejs.org/guide/introduction.html#composition-api).
9
+ *
10
+ * Its corresponding renderless component is {@link GraffitiHandleToActor}.
11
+ *
12
+ * The arguments of this composable are the same as Graffiti.handleToActor,
13
+ * only they can also be [Refs](https://vuejs.org/api/reactivity-core.html#ref)
14
+ * or [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#description).
15
+ * As they change the output will automatically update.
16
+ * Reactivity only triggers when the root array or object changes,
17
+ * not when the elements or properties change.
18
+ * If you need deep reactivity, wrap your argument in a getter.
19
+ *
20
+ * @returns
21
+ * - `actor`: A [ref](https://vuejs.org/api/reactivity-core.html#ref) that contains
22
+ * the retrieved actor, if it exists. If the actor cannot be found, the result
23
+ * is `null`. If the actor is still being fetched, the result is `undefined`.
24
+ */
25
+ export function useGraffitiHandleToActor(handle: MaybeRefOrGetter<string>) {
26
+ const graffiti = useGraffiti();
27
+ const { output } = useResolveString(
28
+ handle,
29
+ graffiti.handleToActor.bind(graffiti),
30
+ );
31
+ return { actor: output };
32
+ }
@@ -0,0 +1,46 @@
1
+ import { GraffitiErrorNotFound } from "@graffiti-garden/api";
2
+ import type { MaybeRefOrGetter } from "vue";
3
+ import { ref, toValue, watch } from "vue";
4
+
5
+ export function useResolveString(
6
+ input: MaybeRefOrGetter<string>,
7
+ resolve: (input: string) => Promise<string>,
8
+ ) {
9
+ const output = ref<string | null | undefined>(undefined);
10
+
11
+ watch(
12
+ () => toValue(input),
13
+ async (input, _prev, onInvalidate) => {
14
+ let active = true;
15
+ onInvalidate(() => {
16
+ active = false;
17
+ });
18
+
19
+ output.value = undefined;
20
+
21
+ try {
22
+ const resolved = await resolve(input);
23
+ if (active) output.value = resolved;
24
+ } catch (err) {
25
+ if (!active) return;
26
+
27
+ if (err instanceof GraffitiErrorNotFound) {
28
+ output.value = null;
29
+ } else {
30
+ console.error(err);
31
+ }
32
+ }
33
+ },
34
+ { immediate: true },
35
+ );
36
+
37
+ return {
38
+ output,
39
+ };
40
+ }
41
+
42
+ export function displayOutput(output: string | null | undefined) {
43
+ if (output === undefined) return "Loading...";
44
+ if (output === null) return "Not found";
45
+ return output;
46
+ }