@graffiti-garden/wrapper-vue 0.7.2 → 1.0.3
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/README.md +2 -3
- package/dist/browser/ajv-D_HICdxS.mjs +4447 -0
- package/dist/browser/ajv-D_HICdxS.mjs.map +1 -0
- package/dist/browser/plugin.mjs +999 -943
- package/dist/browser/plugin.mjs.map +1 -1
- package/dist/node/components/ActorToHandle.vue.d.ts +23 -0
- package/dist/node/components/ActorToHandle.vue.d.ts.map +1 -0
- package/dist/node/{Discover.vue.d.ts → components/Discover.vue.d.ts} +4 -4
- package/dist/node/components/Discover.vue.d.ts.map +1 -0
- package/dist/node/{Get.vue.d.ts → components/Get.vue.d.ts} +2 -5
- package/dist/node/components/Get.vue.d.ts.map +1 -0
- package/dist/node/components/GetMedia.vue.d.ts +36 -0
- package/dist/node/components/GetMedia.vue.d.ts.map +1 -0
- package/dist/node/components/HandleToActor.vue.d.ts +23 -0
- package/dist/node/components/HandleToActor.vue.d.ts.map +1 -0
- package/dist/node/components/ObjectInfo.vue.d.ts +7 -0
- package/dist/node/components/ObjectInfo.vue.d.ts.map +1 -0
- package/dist/node/composables/actor-to-handle.d.ts +25 -0
- package/dist/node/composables/actor-to-handle.d.ts.map +1 -0
- package/dist/node/composables/discover.d.ts +38 -0
- package/dist/node/composables/discover.d.ts.map +1 -0
- package/dist/node/composables/get-media.d.ts +31 -0
- package/dist/node/composables/get-media.d.ts.map +1 -0
- package/dist/node/composables/get.d.ts +28 -0
- package/dist/node/composables/get.d.ts.map +1 -0
- package/dist/node/composables/handle-to-actor.d.ts +25 -0
- package/dist/node/composables/handle-to-actor.d.ts.map +1 -0
- package/dist/node/composables/resolve-string.d.ts +6 -0
- package/dist/node/composables/resolve-string.d.ts.map +1 -0
- package/dist/node/globals.d.ts +3 -5
- package/dist/node/globals.d.ts.map +1 -1
- package/dist/node/plugin.d.ts +174 -75
- package/dist/node/plugin.d.ts.map +1 -1
- package/dist/node/plugin.js +1 -1
- package/dist/node/plugin.js.map +1 -1
- package/dist/node/plugin.mjs +468 -333
- package/dist/node/plugin.mjs.map +1 -1
- package/package.json +15 -14
- package/src/components/ActorToHandle.vue +16 -0
- package/src/{Discover.vue → components/Discover.vue} +15 -9
- package/src/{Get.vue → components/Get.vue} +7 -11
- package/src/components/GetMedia.vue +75 -0
- package/src/components/HandleToActor.vue +16 -0
- package/src/components/ObjectInfo.vue +127 -0
- package/src/composables/actor-to-handle.ts +32 -0
- package/src/composables/discover.ts +202 -0
- package/src/composables/get-media.ts +116 -0
- package/src/composables/get.ts +109 -0
- package/src/composables/handle-to-actor.ts +32 -0
- package/src/composables/resolve-string.ts +46 -0
- package/src/globals.ts +24 -2
- package/src/plugin.ts +84 -29
- package/dist/browser/ajv-C30pimY5.mjs +0 -4400
- package/dist/browser/ajv-C30pimY5.mjs.map +0 -1
- package/dist/browser/index-CWfNKdDL.mjs +0 -424
- package/dist/browser/index-CWfNKdDL.mjs.map +0 -1
- package/dist/node/Discover.vue.d.ts.map +0 -1
- package/dist/node/Get.vue.d.ts.map +0 -1
- package/dist/node/RecoverOrphans.vue.d.ts +0 -31
- package/dist/node/RecoverOrphans.vue.d.ts.map +0 -1
- package/dist/node/composables.d.ts +0 -75
- package/dist/node/composables.d.ts.map +0 -1
- package/dist/node/pollers.d.ts +0 -28
- package/dist/node/pollers.d.ts.map +0 -1
- package/dist/node/reducers.d.ts +0 -37
- package/dist/node/reducers.d.ts.map +0 -1
- package/src/RecoverOrphans.vue +0 -37
- package/src/composables.ts +0 -347
- package/src/pollers.ts +0 -119
- 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
|
+
GraffitiMediaRequirements,
|
|
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
|
+
requirements: MaybeRefOrGetter<GraffitiMediaRequirements>,
|
|
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(requirements), 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
|
+
}
|