@owdproject/nuxt-module-atproto-persistence 4.0.0-alpha.3 → 4.0.0-alpha.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.
- package/README.md +89 -13
- package/dist/module.json +1 -1
- package/dist/runtime/plugin.js +254 -79
- package/dist/runtime/utils/utilAtprotoApplicationStates.d.ts +30 -7
- package/dist/runtime/utils/utilAtprotoApplicationStates.js +78 -17
- package/dist/runtime/utils/utilAtprotoPersistenceLoad.d.ts +3 -4
- package/dist/runtime/utils/utilAtprotoPersistenceLoad.js +31 -43
- package/dist/runtime/utils/utilAtprotoWriteQueue.d.ts +9 -0
- package/dist/runtime/utils/utilAtprotoWriteQueue.js +57 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -14,19 +14,42 @@ replaces `@owdproject/nuxt-module-pinia-persistence` — **do not install both**
|
|
|
14
14
|
desktop.
|
|
15
15
|
|
|
16
16
|
Boot order is handled without core coupling: the plugin runs in Nuxt's **pre**
|
|
17
|
-
phase (registers the adapter only). Remote hydration runs
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
phase (registers the adapter only). Stores hydrate locally first. Remote hydration runs at `app:created`, after
|
|
18
|
+
the `atproto` OAuth plugin has finished initialization. The Nuxt application
|
|
19
|
+
controllers await `$persistedState.isReady()` before restoring local windows.
|
|
20
20
|
|
|
21
21
|
## Requirements
|
|
22
22
|
|
|
23
|
-
- `@owdproject/
|
|
24
|
-
|
|
23
|
+
- The matching `@owdproject/nuxt` adapter revision with persisted store opt-ins
|
|
24
|
+
and `ApplicationController.restoreApplication()` (release these together).
|
|
25
|
+
- `@owdproject/nuxt-module-atproto` (provides OAuth session and agents)
|
|
26
|
+
|
|
27
|
+
### OAuth scope
|
|
28
|
+
|
|
29
|
+
The special `atproto` scope only covers identity. Writing the desktop profile to
|
|
30
|
+
the repository requires explicit **`repo:`** permissions *with actions* (the PDS
|
|
31
|
+
checks `action=create`/`action=update` for `putRecord`), otherwise the write is
|
|
32
|
+
rejected (403 / `ScopeMissingError`):
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// desktop/nuxt.config.ts
|
|
36
|
+
const ACTIONS = 'action=create&action=update&action=delete'
|
|
37
|
+
atproto: {
|
|
38
|
+
oauth: {
|
|
39
|
+
signInOptions: { scope: `atproto repo:org.owdproject.desktop?${ACTIONS} repo:org.owdproject.application.windows?${ACTIONS} repo:org.owdproject.application.meta?${ACTIONS}` },
|
|
40
|
+
clientMetadata: { local: { scope: `atproto repo:org.owdproject.desktop?${ACTIONS} repo:org.owdproject.application.windows?${ACTIONS} repo:org.owdproject.application.meta?${ACTIONS}` } },
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can use `repo:*?action=create&action=update&action=delete` instead to grant
|
|
46
|
+
access to every record type. Changing the scope requires signing in again
|
|
47
|
+
(existing sessions keep their original grant).
|
|
25
48
|
|
|
26
49
|
## Installation
|
|
27
50
|
|
|
28
51
|
```bash
|
|
29
|
-
pnpm desktop add module-atproto-persistence
|
|
52
|
+
pnpm desktop add nuxt-module-atproto-persistence
|
|
30
53
|
```
|
|
31
54
|
|
|
32
55
|
## Configuration
|
|
@@ -35,8 +58,8 @@ pnpm desktop add module-atproto-persistence
|
|
|
35
58
|
// desktop/desktop.config.ts
|
|
36
59
|
export default defineDesktopConfig({
|
|
37
60
|
modules: [
|
|
38
|
-
'@owdproject/module-atproto',
|
|
39
|
-
'@owdproject/module-atproto-persistence',
|
|
61
|
+
'@owdproject/nuxt-module-atproto',
|
|
62
|
+
'@owdproject/nuxt-module-atproto-persistence',
|
|
40
63
|
],
|
|
41
64
|
atprotoPersistence: {
|
|
42
65
|
// Load atprotoDesktop.owner.did remote state on boot (not only /actor/:did)
|
|
@@ -47,7 +70,7 @@ export default defineDesktopConfig({
|
|
|
47
70
|
|
|
48
71
|
## Actor resolution (remote hydrate)
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
After plugin initialization, remote state is fetched when an actor DID is
|
|
51
74
|
resolved:
|
|
52
75
|
|
|
53
76
|
1. Route `/actor/:did` — view another actor's desktop
|
|
@@ -55,16 +78,69 @@ resolved:
|
|
|
55
78
|
3. `loadOwnerDesktopOnMounted: true` — `atprotoDesktop.owner.did`
|
|
56
79
|
4. Otherwise — IndexedDB only
|
|
57
80
|
|
|
81
|
+
## Desktop scoping
|
|
82
|
+
|
|
83
|
+
Persisted state is namespaced by the **desktop distribution id**, declared as
|
|
84
|
+
`id` in `desktop.config.ts`:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
export default defineDesktopConfig({
|
|
88
|
+
id: 'atproto-os',
|
|
89
|
+
// ...
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The id is resolved from `runtimeConfig.public.desktop.id` (falling back to the
|
|
94
|
+
reversibly encoded branded title/affix, then `name`). It prefixes the AT Protocol record rkeys and
|
|
95
|
+
the local IndexedDB keys, so products configured with distinct stable ids do not share a profile.
|
|
96
|
+
Using the same id deliberately shares that namespace for the same account. Records that do not carry the current desktop prefix are ignored.
|
|
97
|
+
|
|
58
98
|
## AT Protocol collections
|
|
59
99
|
|
|
60
100
|
| Pinia store id | AT Proto collection | rkey |
|
|
61
101
|
|---|---|---|
|
|
62
|
-
| `desktop` | `org.owdproject.desktop` |
|
|
63
|
-
| `desktop
|
|
64
|
-
| `desktop/application/<appId>/
|
|
102
|
+
| `desktop` | `org.owdproject.desktop` | `<desktopId>` |
|
|
103
|
+
| `desktop/<sub>` | `org.owdproject.desktop` | `<desktopId>:<encoded sub>` |
|
|
104
|
+
| `desktop/application/<appId>/windows` | `org.owdproject.application.windows` | `<desktopId>:<encoded appId>` |
|
|
105
|
+
| `desktop/application/<appId>/meta` | `org.owdproject.application.meta` | `<desktopId>:<encoded appId>` |
|
|
65
106
|
|
|
66
107
|
When logged in, mapped keys are written to the repo on change (`putRecord`).
|
|
67
|
-
Unmapped keys stay local-only.
|
|
108
|
+
Unmapped keys stay local-only. Local IndexedDB keys are `<desktopId>/<pinia store id>`.
|
|
109
|
+
|
|
110
|
+
## Sign-out
|
|
111
|
+
|
|
112
|
+
On sign-out the module forgets the cached remote state map and wipes the local
|
|
113
|
+
IndexedDB keys for the desktop (`atproto:sessionDeleted`, or the
|
|
114
|
+
`authenticated → anonymous` transition). The desktop shell is expected to close
|
|
115
|
+
its windows on the same lifecycle event so the next account never inherits the
|
|
116
|
+
previous one's layout (see the atproto OS host plugin
|
|
117
|
+
`atproto-reset-on-signout.client.ts`).
|
|
118
|
+
|
|
119
|
+
## Restore and delivery guarantees
|
|
120
|
+
|
|
121
|
+
The adapter registers synchronously before stores are created. Stores first
|
|
122
|
+
hydrate from IndexedDB. Remote hydration runs at `app:created`, after OAuth
|
|
123
|
+
initialization finishes, without treating a slow login as a guest session.
|
|
124
|
+
Session changes and network recovery can retry failed reads. Remote hydration
|
|
125
|
+
keeps changes made locally while a read was in flight, and a repeated session
|
|
126
|
+
hook does not reopen closed windows.
|
|
127
|
+
|
|
128
|
+
Authenticated changes are saved to a desktop/account-scoped IndexedDB outbox.
|
|
129
|
+
Remote writes are serialized per store; failures retry after five seconds or
|
|
130
|
+
on network recovery. An entry is removed only after a successful write of that
|
|
131
|
+
value. Pending entries survive reloads and take precedence over older remote
|
|
132
|
+
records. This is snapshot persistence, not real-time collaboration; Jetstream
|
|
133
|
+
integration remains separate.
|
|
134
|
+
|
|
135
|
+
Explicit desktop ids preserve case and punctuation and must contain 1–64 ASCII
|
|
136
|
+
letters, digits, dots, underscores or hyphens, starting with a letter or digit.
|
|
137
|
+
Use a stable explicit id for each distribution. Hosts without one retain a
|
|
138
|
+
reversibly encoded branding fallback; changing that branding changes the profile
|
|
139
|
+
namespace. Record-key components encode reserved characters reversibly, including
|
|
140
|
+
multi-segment store ids. Reads paginate through all collection records.
|
|
141
|
+
|
|
142
|
+
Run `pnpm run test:persistence` after installing dev dependencies to verify the
|
|
143
|
+
identity mapping, pagination, slow OAuth, remote restore and offline write retry.
|
|
68
144
|
|
|
69
145
|
## License
|
|
70
146
|
|
package/dist/module.json
CHANGED
package/dist/runtime/plugin.js
CHANGED
|
@@ -1,100 +1,275 @@
|
|
|
1
1
|
import { createPersistedStatePlugin } from "pinia-plugin-persistedstate-2";
|
|
2
2
|
import { defineNuxtPlugin } from "nuxt/app";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const keys2 = Object.keys(obj2);
|
|
8
|
-
if (keys1.length !== keys2.length) return false;
|
|
9
|
-
for (const key of keys1) {
|
|
10
|
-
if (!Object.prototype.hasOwnProperty.call(obj2, key)) return false;
|
|
11
|
-
if (!deepEqual(obj1[key], obj2[key])) return false;
|
|
12
|
-
}
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
import { useAtprotoSession, useAtprotoAgent } from "#imports";
|
|
16
|
-
import { get, set, del } from "idb-keyval";
|
|
3
|
+
import { nextTick, watch } from "vue";
|
|
4
|
+
import { useAtprotoAgent, useAtprotoSession } from "#imports";
|
|
5
|
+
import { useDesktopApplication } from "@owdproject/nuxt/runtime/composables/useDesktopApplication";
|
|
6
|
+
import { get, set, del, keys } from "idb-keyval";
|
|
17
7
|
import {
|
|
8
|
+
parseAtprotoStoreKey,
|
|
18
9
|
putAtprotoApplicationState,
|
|
19
|
-
|
|
10
|
+
resolveDesktopId,
|
|
11
|
+
toLocalStorageKey
|
|
20
12
|
} from "./utils/utilAtprotoApplicationStates.js";
|
|
21
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ensureActorStatesLoaded,
|
|
15
|
+
resolveRestoreActorDid,
|
|
16
|
+
invalidateActorStates
|
|
17
|
+
} from "./utils/utilAtprotoPersistenceLoad.js";
|
|
18
|
+
import { createAtprotoWriteQueue } from "./utils/utilAtprotoWriteQueue.js";
|
|
22
19
|
function mapStoreKey(key) {
|
|
23
|
-
if (key === "desktop")
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (key.startsWith("desktop/application/")) {
|
|
20
|
+
if (key === "desktop") return "owd/desktop";
|
|
21
|
+
if (key.startsWith("desktop/application/"))
|
|
27
22
|
return key.replace(/^desktop\/application\//, "owd/application/");
|
|
28
|
-
|
|
29
|
-
if (key.startsWith("desktop/")) {
|
|
23
|
+
if (key.startsWith("desktop/"))
|
|
30
24
|
return key.replace(/^desktop\//, "owd/desktop/");
|
|
31
|
-
}
|
|
32
25
|
return key;
|
|
33
26
|
}
|
|
27
|
+
function replaceState(store, value) {
|
|
28
|
+
store.$patch((state) => {
|
|
29
|
+
for (const [key, next] of Object.entries(value)) {
|
|
30
|
+
const current = state[key];
|
|
31
|
+
if (current && next && !Array.isArray(current) && !Array.isArray(next) && typeof current === "object" && typeof next === "object") {
|
|
32
|
+
for (const old of Object.keys(current)) delete current[old];
|
|
33
|
+
Object.assign(current, next);
|
|
34
|
+
} else {
|
|
35
|
+
state[key] = next;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
34
40
|
export default defineNuxtPlugin({
|
|
35
41
|
name: "desktop-plugin-atproto-persistence",
|
|
36
|
-
dependsOn: ["pinia"
|
|
42
|
+
dependsOn: ["pinia"],
|
|
37
43
|
enforce: "pre",
|
|
38
|
-
|
|
44
|
+
setup(nuxtApp) {
|
|
39
45
|
const pinia = nuxtApp.$pinia;
|
|
40
|
-
if (!pinia)
|
|
41
|
-
|
|
46
|
+
if (!pinia) return;
|
|
47
|
+
const desktopId = resolveDesktopId(nuxtApp);
|
|
48
|
+
const stores = /* @__PURE__ */ new Map();
|
|
49
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
50
|
+
const localWrites = /* @__PURE__ */ new Map();
|
|
51
|
+
const versions = /* @__PURE__ */ new Map();
|
|
52
|
+
let hydrating = false;
|
|
53
|
+
let armed = false;
|
|
54
|
+
let booted = false;
|
|
55
|
+
let currentDid;
|
|
56
|
+
let scopeWarned = false;
|
|
57
|
+
let resetTask;
|
|
58
|
+
let stopSessionWatch;
|
|
59
|
+
let retry;
|
|
60
|
+
let syncTask;
|
|
61
|
+
let restoredActor;
|
|
62
|
+
const localKey = (key) => toLocalStorageKey(desktopId, key);
|
|
63
|
+
const outboxPrefix = (did) => `${desktopId}/outbox/${did}/`;
|
|
64
|
+
function authenticatedDid() {
|
|
65
|
+
if (!nuxtApp.$atproto) return void 0;
|
|
66
|
+
const { isLogged, session } = nuxtApp.runWithContext(
|
|
67
|
+
() => useAtprotoSession()
|
|
68
|
+
);
|
|
69
|
+
return isLogged.value ? session.value?.sub : void 0;
|
|
42
70
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
function createQueue() {
|
|
72
|
+
return createAtprotoWriteQueue(
|
|
73
|
+
async (key, value) => {
|
|
74
|
+
const did = authenticatedDid();
|
|
75
|
+
if (!did || did !== currentDid)
|
|
76
|
+
throw new Error("AT Protocol session changed before sync");
|
|
77
|
+
const target = parseAtprotoStoreKey(key, desktopId);
|
|
78
|
+
if (!target) return;
|
|
79
|
+
const agent = nuxtApp.runWithContext(
|
|
80
|
+
() => useAtprotoAgent("authenticated")
|
|
81
|
+
);
|
|
82
|
+
await putAtprotoApplicationState(
|
|
83
|
+
agent,
|
|
84
|
+
did,
|
|
85
|
+
target.collection,
|
|
86
|
+
target.rkey,
|
|
87
|
+
JSON.parse(value)
|
|
88
|
+
);
|
|
89
|
+
const outboxKey = `${outboxPrefix(did)}${key}`;
|
|
90
|
+
if (await get(outboxKey) === value) await del(outboxKey);
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
onError: (error) => {
|
|
94
|
+
if (!scopeWarned) {
|
|
95
|
+
scopeWarned = true;
|
|
96
|
+
console.warn(
|
|
97
|
+
"[OWD persistence] Remote save failed; queued locally for retry. For ScopeMissingError, request the matching repo: permissions in OAuth metadata and sign in again.",
|
|
98
|
+
error
|
|
99
|
+
);
|
|
63
100
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
let queue = createQueue();
|
|
106
|
+
const adapter = createPersistedStatePlugin({
|
|
107
|
+
persist: false,
|
|
108
|
+
assertStorage: () => {
|
|
109
|
+
},
|
|
110
|
+
storage: {
|
|
111
|
+
getItem: async (key) => await get(localKey(key)) ?? null,
|
|
112
|
+
setItem(key, value) {
|
|
113
|
+
if (resetTask) return resetTask;
|
|
114
|
+
if (!hydrating) versions.set(key, (versions.get(key) ?? 0) + 1);
|
|
115
|
+
const did = armed && !hydrating ? authenticatedDid() : void 0;
|
|
116
|
+
const previous = localWrites.get(key) ?? Promise.resolve();
|
|
117
|
+
const task = previous.catch(() => {
|
|
118
|
+
}).then(async () => {
|
|
119
|
+
await set(localKey(key), value);
|
|
120
|
+
if (did && did === currentDid && parseAtprotoStoreKey(key, desktopId)) {
|
|
121
|
+
await set(`${outboxPrefix(did)}${key}`, value);
|
|
122
|
+
void queue.enqueue(key, value);
|
|
79
123
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
124
|
+
});
|
|
125
|
+
localWrites.set(key, task);
|
|
126
|
+
return task;
|
|
127
|
+
},
|
|
128
|
+
removeItem: async (key) => {
|
|
129
|
+
await del(localKey(key));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
pinia.use((context) => {
|
|
134
|
+
const initial = JSON.stringify(context.store.$state);
|
|
135
|
+
const result = adapter(context);
|
|
136
|
+
if (context.store.$persistedState) {
|
|
137
|
+
stores.set(context.store.$id, context.store);
|
|
138
|
+
defaults.set(context.store.$id, initial);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
});
|
|
142
|
+
async function syncRemote() {
|
|
143
|
+
await Promise.all(
|
|
144
|
+
[...stores.values()].map((store) => store.$persistedState.isReady())
|
|
145
|
+
);
|
|
146
|
+
await Promise.all(localWrites.values());
|
|
147
|
+
const baseline = new Map(versions);
|
|
148
|
+
const actor = await resolveRestoreActorDid(nuxtApp);
|
|
149
|
+
const did = authenticatedDid();
|
|
150
|
+
if (currentDid !== did) {
|
|
151
|
+
queue.stop();
|
|
152
|
+
queue = createQueue();
|
|
153
|
+
}
|
|
154
|
+
currentDid = did;
|
|
155
|
+
if (restoredActor === (actor ?? "guest")) return;
|
|
156
|
+
try {
|
|
157
|
+
const remote = await ensureActorStatesLoaded(nuxtApp, true);
|
|
158
|
+
if (actor !== await resolveRestoreActorDid(nuxtApp) || did !== authenticatedDid())
|
|
159
|
+
return;
|
|
160
|
+
await Promise.all(
|
|
161
|
+
[...stores.values()].map((store) => store.$persistedState.isReady())
|
|
162
|
+
);
|
|
163
|
+
await Promise.all(localWrites.values());
|
|
164
|
+
const applications = nuxtApp.runWithContext(
|
|
165
|
+
() => useDesktopApplication()
|
|
166
|
+
);
|
|
167
|
+
hydrating = true;
|
|
168
|
+
try {
|
|
169
|
+
for (const [key, store] of stores) {
|
|
170
|
+
const pending = did ? await get(`${outboxPrefix(did)}${key}`) : void 0;
|
|
171
|
+
const value = pending ? JSON.parse(pending) : remote[mapStoreKey(key)];
|
|
172
|
+
if (!value || (versions.get(key) ?? 0) !== (baseline.get(key) ?? 0))
|
|
173
|
+
continue;
|
|
174
|
+
const match = /^desktop\/application\/(.+)\/windows$/.exec(key);
|
|
175
|
+
const app = match ? applications.get(match[1]) : void 0;
|
|
176
|
+
app?.closeAllWindows();
|
|
177
|
+
replaceState(store, value);
|
|
178
|
+
if (app) await app.restoreApplication();
|
|
95
179
|
}
|
|
180
|
+
} finally {
|
|
181
|
+
await nextTick();
|
|
182
|
+
hydrating = false;
|
|
183
|
+
}
|
|
184
|
+
await Promise.all(localWrites.values());
|
|
185
|
+
armed = true;
|
|
186
|
+
restoredActor = actor ?? "guest";
|
|
187
|
+
if (did) {
|
|
188
|
+
for (const key of await keys()) {
|
|
189
|
+
if (typeof key !== "string" || !key.startsWith(outboxPrefix(did)))
|
|
190
|
+
continue;
|
|
191
|
+
const value = await get(key);
|
|
192
|
+
if (value != null)
|
|
193
|
+
void queue.enqueue(key.slice(outboxPrefix(did).length), value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
armed = true;
|
|
198
|
+
console.warn(
|
|
199
|
+
"[OWD persistence] Remote restore failed; keeping local windows and retrying."
|
|
200
|
+
);
|
|
201
|
+
if (!retry)
|
|
202
|
+
retry = setTimeout(() => {
|
|
203
|
+
retry = void 0;
|
|
204
|
+
void synchronize();
|
|
205
|
+
}, 5e3);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function synchronize() {
|
|
209
|
+
if (!booted) return Promise.resolve();
|
|
210
|
+
if (!syncTask)
|
|
211
|
+
syncTask = syncRemote().finally(() => {
|
|
212
|
+
syncTask = void 0;
|
|
213
|
+
});
|
|
214
|
+
return syncTask;
|
|
215
|
+
}
|
|
216
|
+
nuxtApp.hook("app:created", async () => {
|
|
217
|
+
booted = true;
|
|
218
|
+
await synchronize();
|
|
219
|
+
});
|
|
220
|
+
const lifecycle = nuxtApp.hook;
|
|
221
|
+
lifecycle("atproto:sessionCreated", () => synchronize());
|
|
222
|
+
lifecycle("atproto:sessionRestored", () => synchronize());
|
|
223
|
+
function resetPersistence() {
|
|
224
|
+
if (resetTask) return resetTask;
|
|
225
|
+
armed = false;
|
|
226
|
+
currentDid = void 0;
|
|
227
|
+
restoredActor = void 0;
|
|
228
|
+
invalidateActorStates(nuxtApp);
|
|
229
|
+
queue.stop();
|
|
230
|
+
queue = createQueue();
|
|
231
|
+
if (retry) clearTimeout(retry);
|
|
232
|
+
retry = void 0;
|
|
233
|
+
const pending = [...localWrites.values()];
|
|
234
|
+
resetTask = (async () => {
|
|
235
|
+
await nextTick();
|
|
236
|
+
for (const [key, store] of stores) {
|
|
237
|
+
const initial = defaults.get(key);
|
|
238
|
+
if (initial) replaceState(store, JSON.parse(initial));
|
|
239
|
+
}
|
|
240
|
+
await nextTick();
|
|
241
|
+
await Promise.allSettled(pending);
|
|
242
|
+
for (const key of await keys()) {
|
|
243
|
+
if (typeof key === "string" && key.startsWith(`${desktopId}/`))
|
|
244
|
+
await del(key);
|
|
245
|
+
}
|
|
246
|
+
})().finally(() => {
|
|
247
|
+
resetTask = void 0;
|
|
248
|
+
});
|
|
249
|
+
return resetTask;
|
|
250
|
+
}
|
|
251
|
+
lifecycle("atproto:sessionDeleted", resetPersistence);
|
|
252
|
+
nuxtApp.hook("app:mounted", () => {
|
|
253
|
+
const atproto = nuxtApp.$atproto;
|
|
254
|
+
if (!atproto) return;
|
|
255
|
+
stopSessionWatch = watch(
|
|
256
|
+
() => atproto.status?.value,
|
|
257
|
+
(next, previous) => {
|
|
258
|
+
if (previous === "authenticated" && next !== "authenticated")
|
|
259
|
+
void resetPersistence();
|
|
96
260
|
}
|
|
97
|
-
|
|
98
|
-
);
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
const online = () => {
|
|
264
|
+
void queue.flush();
|
|
265
|
+
void synchronize();
|
|
266
|
+
};
|
|
267
|
+
window.addEventListener("online", online);
|
|
268
|
+
nuxtApp.vueApp.onUnmount?.(() => {
|
|
269
|
+
queue.stop();
|
|
270
|
+
stopSessionWatch?.();
|
|
271
|
+
if (retry) clearTimeout(retry);
|
|
272
|
+
window.removeEventListener("online", online);
|
|
273
|
+
});
|
|
99
274
|
}
|
|
100
275
|
});
|
|
@@ -1,24 +1,47 @@
|
|
|
1
|
+
/** Reversible record-key encoding; AT Protocol permits `~`, but not `/` or `%`. */
|
|
2
|
+
export declare function encodeRecordKeyPart(value: string): string;
|
|
1
3
|
/** Resolve PDS endpoint via PLC directory (Bluesky); custom PDSs use the same DID document shape. */
|
|
2
4
|
export declare function resolveActorServiceEndpoint(did: string): Promise<string>;
|
|
5
|
+
/** Slug used inside rkeys (RecordKey forbids `/`, so desktop state is namespaced with this). */
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
7
|
+
* Identity of the desktop distribution that owns the persisted profile.
|
|
8
|
+
*
|
|
9
|
+
* Declared as `id` in `desktop.config.ts` (`defineDesktopConfig({ id: 'atproto-os' })`)
|
|
10
|
+
* and surfaced on `runtimeConfig.public.desktop`. Falling back to the branded
|
|
11
|
+
* title/affix keeps two different OWD distributions from sharing records when an
|
|
12
|
+
* explicit `id` is missing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveDesktopId(nuxtApp: any): string;
|
|
15
|
+
/** IndexedDB key for a Pinia store id, scoped to the current desktop distribution. */
|
|
16
|
+
export declare function toLocalStorageKey(desktopId: string, piniaKey: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Maps a Pinia store id to an AT Protocol collection and desktop-scoped rkey.
|
|
19
|
+
*
|
|
20
|
+
* The rkey is prefixed with the desktop id so two different OWD distributions
|
|
21
|
+
* (e.g. "Marco's OWD" vs "Alessio's OWD") never read or overwrite each other's
|
|
22
|
+
* profile, even for the same logged-in user.
|
|
5
23
|
*
|
|
6
24
|
* Key format → AT Proto mapping:
|
|
7
|
-
* - `desktop/application/<appId>/windows` → collection: `org.owdproject.application.windows`, rkey: `<appId>`
|
|
8
|
-
* - `desktop/application/<appId>/meta` → collection: `org.owdproject.application.meta`, rkey: `<appId>`
|
|
9
|
-
* - `desktop`
|
|
25
|
+
* - `desktop/application/<appId>/windows` → collection: `org.owdproject.application.windows`, rkey: `<desktopId>:<appId>`
|
|
26
|
+
* - `desktop/application/<appId>/meta` → collection: `org.owdproject.application.meta`, rkey: `<desktopId>:<appId>`
|
|
27
|
+
* - `desktop` → collection: `org.owdproject.desktop`, rkey: `<desktopId>`
|
|
28
|
+
* - `desktop/<sub>` → collection: `org.owdproject.desktop`, rkey: `<desktopId>:<sub>`
|
|
10
29
|
*
|
|
11
30
|
* Returns `null` for keys that are not persisted to AT Proto.
|
|
12
31
|
*/
|
|
13
|
-
export declare function parseAtprotoStoreKey(key: string): {
|
|
32
|
+
export declare function parseAtprotoStoreKey(key: string, desktopId: string): {
|
|
14
33
|
collection: string;
|
|
15
34
|
rkey: string;
|
|
16
35
|
} | null;
|
|
17
|
-
export declare function listAtprotoApplicationStateRecords(agent: any, repo: string, collection: string): any
|
|
36
|
+
export declare function listAtprotoApplicationStateRecords(agent: any, repo: string, collection: string): Promise<any[]>;
|
|
18
37
|
export declare function getAtprotoApplicationState(agent: any, repo: string, collection: string, rkey: string): any;
|
|
19
38
|
export declare function putAtprotoApplicationState(agent: any, repo: string, collection: string, rkey: string, record: any): any;
|
|
20
39
|
/**
|
|
21
40
|
* Loads the full desktop state map for a given actor from the AT Protocol
|
|
22
41
|
* repository. Returns a map keyed by Pinia store id (`desktop/` prefix).
|
|
42
|
+
*
|
|
43
|
+
* Only records belonging to `desktopId` are considered: rkeys must be exactly
|
|
44
|
+
* `<desktopId>` or start with `<desktopId>:`. Legacy/foreign records are ignored,
|
|
45
|
+
* so opening another distribution's OWD never loads an unrelated profile.
|
|
23
46
|
*/
|
|
24
|
-
export declare function loadActorDesktopStateMap(nuxtApp: any, actorDid: string): Promise<Record<string, any>>;
|
|
47
|
+
export declare function loadActorDesktopStateMap(nuxtApp: any, actorDid: string, desktopId: string): Promise<Record<string, any>>;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "nuxt/app";
|
|
1
2
|
import { useAtprotoAgent, useAtprotoSession } from "#imports";
|
|
3
|
+
export function encodeRecordKeyPart(value) {
|
|
4
|
+
return Array.from(new TextEncoder().encode(value), (byte) => {
|
|
5
|
+
const char = String.fromCharCode(byte);
|
|
6
|
+
return /[a-zA-Z0-9._-]/.test(char) ? char : `~${byte.toString(16).padStart(2, "0")}`;
|
|
7
|
+
}).join("");
|
|
8
|
+
}
|
|
9
|
+
function decodeRecordKeyPart(value) {
|
|
10
|
+
return decodeURIComponent(value.replace(/~/g, "%"));
|
|
11
|
+
}
|
|
2
12
|
export async function resolveActorServiceEndpoint(did) {
|
|
3
13
|
const response = await fetch(`https://plc.directory/${did}`);
|
|
4
14
|
if (!response.ok) {
|
|
@@ -7,7 +17,24 @@ export async function resolveActorServiceEndpoint(did) {
|
|
|
7
17
|
const data = await response.json();
|
|
8
18
|
return data.service[0].serviceEndpoint;
|
|
9
19
|
}
|
|
10
|
-
export function
|
|
20
|
+
export function resolveDesktopId(nuxtApp) {
|
|
21
|
+
const runtimeConfig = nuxtApp.runWithContext(() => useRuntimeConfig());
|
|
22
|
+
const desktop = runtimeConfig.public?.desktop ?? {};
|
|
23
|
+
if (typeof desktop.id === "string" && desktop.id.length > 0) {
|
|
24
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(desktop.id)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"[OWD persistence] Invalid desktop id; use 1\u201364 ASCII letters, digits, dots, underscores or hyphens."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return desktop.id;
|
|
30
|
+
}
|
|
31
|
+
const branded = [desktop.title, desktop.affix].filter(Boolean).join("-");
|
|
32
|
+
return `legacy-${encodeRecordKeyPart(branded || desktop.name || "owd")}`;
|
|
33
|
+
}
|
|
34
|
+
export function toLocalStorageKey(desktopId, piniaKey) {
|
|
35
|
+
return `${desktopId}/${piniaKey}`;
|
|
36
|
+
}
|
|
37
|
+
export function parseAtprotoStoreKey(key, desktopId) {
|
|
11
38
|
if (key.startsWith("desktop/application/")) {
|
|
12
39
|
const parts = key.split("/");
|
|
13
40
|
const last = parts[parts.length - 1];
|
|
@@ -15,26 +42,39 @@ export function parseAtprotoStoreKey(key) {
|
|
|
15
42
|
const appId = parts.slice(2, -1).join("/");
|
|
16
43
|
return {
|
|
17
44
|
collection: `org.owdproject.application.${last}`,
|
|
18
|
-
rkey: appId
|
|
45
|
+
rkey: `${desktopId}:${encodeRecordKeyPart(appId)}`
|
|
19
46
|
};
|
|
20
47
|
}
|
|
21
48
|
}
|
|
22
49
|
if (key === "desktop" || key.startsWith("desktop/")) {
|
|
23
|
-
const sub = key.split("/")[1] ?? "self";
|
|
24
50
|
if (!key.startsWith("desktop/application/")) {
|
|
51
|
+
const sub = key.slice("desktop/".length);
|
|
25
52
|
return {
|
|
26
53
|
collection: "org.owdproject.desktop",
|
|
27
|
-
rkey: sub
|
|
54
|
+
rkey: key === "desktop" ? desktopId : `${desktopId}:${encodeRecordKeyPart(sub)}`
|
|
28
55
|
};
|
|
29
56
|
}
|
|
30
57
|
}
|
|
31
58
|
return null;
|
|
32
59
|
}
|
|
33
|
-
export function listAtprotoApplicationStateRecords(agent, repo, collection) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
export async function listAtprotoApplicationStateRecords(agent, repo, collection) {
|
|
61
|
+
const records = [];
|
|
62
|
+
let cursor;
|
|
63
|
+
const seen = /* @__PURE__ */ new Set();
|
|
64
|
+
do {
|
|
65
|
+
const { data } = await agent.com.atproto.repo.listRecords({
|
|
66
|
+
repo,
|
|
67
|
+
collection,
|
|
68
|
+
cursor,
|
|
69
|
+
limit: 100
|
|
70
|
+
});
|
|
71
|
+
records.push(...data.records);
|
|
72
|
+
cursor = data.cursor;
|
|
73
|
+
if (cursor && seen.has(cursor))
|
|
74
|
+
throw new Error("AT Protocol returned a repeated records cursor");
|
|
75
|
+
if (cursor) seen.add(cursor);
|
|
76
|
+
} while (cursor);
|
|
77
|
+
return records;
|
|
38
78
|
}
|
|
39
79
|
export function getAtprotoApplicationState(agent, repo, collection, rkey) {
|
|
40
80
|
return agent.com.atproto.repo.getRecord({
|
|
@@ -48,19 +88,24 @@ export function putAtprotoApplicationState(agent, repo, collection, rkey, record
|
|
|
48
88
|
repo,
|
|
49
89
|
collection,
|
|
50
90
|
rkey,
|
|
51
|
-
record
|
|
91
|
+
record: { ...record, $type: collection }
|
|
52
92
|
});
|
|
53
93
|
}
|
|
54
|
-
export async function loadActorDesktopStateMap(nuxtApp, actorDid) {
|
|
55
|
-
const { isLogged, session } = nuxtApp.runWithContext(
|
|
94
|
+
export async function loadActorDesktopStateMap(nuxtApp, actorDid, desktopId) {
|
|
95
|
+
const { isLogged, session } = nuxtApp.runWithContext(
|
|
96
|
+
() => useAtprotoSession()
|
|
97
|
+
);
|
|
56
98
|
const isOwner = isLogged.value && session.value?.sub === actorDid;
|
|
57
99
|
let actorAgent;
|
|
58
100
|
if (isOwner) {
|
|
59
101
|
actorAgent = nuxtApp.runWithContext(() => useAtprotoAgent("authenticated"));
|
|
60
102
|
} else {
|
|
61
103
|
const actorServiceEndpoint = await resolveActorServiceEndpoint(actorDid);
|
|
62
|
-
actorAgent = nuxtApp.runWithContext(
|
|
104
|
+
actorAgent = nuxtApp.runWithContext(
|
|
105
|
+
() => useAtprotoAgent(actorServiceEndpoint)
|
|
106
|
+
);
|
|
63
107
|
}
|
|
108
|
+
const prefix = `${desktopId}:`;
|
|
64
109
|
const [
|
|
65
110
|
atprotoActorDesktopList,
|
|
66
111
|
atprotoApplicationWindowsList,
|
|
@@ -82,17 +127,31 @@ export async function loadActorDesktopStateMap(nuxtApp, actorDid) {
|
|
|
82
127
|
"org.owdproject.application.meta"
|
|
83
128
|
)
|
|
84
129
|
]);
|
|
130
|
+
for (const result of [
|
|
131
|
+
atprotoActorDesktopList,
|
|
132
|
+
atprotoApplicationWindowsList,
|
|
133
|
+
atprotoApplicationMetaList
|
|
134
|
+
]) {
|
|
135
|
+
if (result.status === "rejected") throw result.reason;
|
|
136
|
+
}
|
|
85
137
|
const stateMap = {};
|
|
86
138
|
if (atprotoActorDesktopList.status === "fulfilled") {
|
|
87
139
|
for (const record of atprotoActorDesktopList.value) {
|
|
88
140
|
const rkey = record.uri.split("/").pop();
|
|
89
|
-
|
|
90
|
-
|
|
141
|
+
if (rkey === desktopId) {
|
|
142
|
+
const { $type, ...state } = record.value;
|
|
143
|
+
stateMap["owd/desktop"] = state;
|
|
144
|
+
} else if (typeof rkey === "string" && rkey.startsWith(prefix)) {
|
|
145
|
+
const { $type, ...state } = record.value;
|
|
146
|
+
stateMap[`owd/desktop/${decodeRecordKeyPart(rkey.slice(prefix.length))}`] = state;
|
|
147
|
+
}
|
|
91
148
|
}
|
|
92
149
|
}
|
|
93
150
|
if (atprotoApplicationWindowsList.status === "fulfilled") {
|
|
94
151
|
for (const record of atprotoApplicationWindowsList.value) {
|
|
95
|
-
const
|
|
152
|
+
const rkey = record.uri.split("/").pop();
|
|
153
|
+
if (typeof rkey !== "string" || !rkey.startsWith(prefix)) continue;
|
|
154
|
+
const appId = decodeRecordKeyPart(rkey.slice(prefix.length));
|
|
96
155
|
stateMap[`owd/application/${appId}/windows`] = {
|
|
97
156
|
windows: record.value.windows
|
|
98
157
|
};
|
|
@@ -100,7 +159,9 @@ export async function loadActorDesktopStateMap(nuxtApp, actorDid) {
|
|
|
100
159
|
}
|
|
101
160
|
if (atprotoApplicationMetaList.status === "fulfilled") {
|
|
102
161
|
for (const record of atprotoApplicationMetaList.value) {
|
|
103
|
-
const
|
|
162
|
+
const rkey = record.uri.split("/").pop();
|
|
163
|
+
if (typeof rkey !== "string" || !rkey.startsWith(prefix)) continue;
|
|
164
|
+
const appId = decodeRecordKeyPart(rkey.slice(prefix.length));
|
|
104
165
|
stateMap[`owd/application/${appId}/meta`] = {
|
|
105
166
|
...record.value
|
|
106
167
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/** Wait until the nuxt-atproto plugin has finished OAuth init (or timeout as guest). */
|
|
2
|
-
export declare function waitForAtprotoReady(nuxtApp: any): Promise<void>;
|
|
3
1
|
export declare function resolveRestoreActorDid(nuxtApp: any): Promise<string | undefined>;
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export declare function ensureActorStatesLoaded(nuxtApp: any, refresh?: boolean): Promise<Record<string, unknown>>;
|
|
3
|
+
/** Invalidate this host's remote profile when its account signs out. */
|
|
4
|
+
export declare function invalidateActorStates(nuxtApp: object): void;
|
|
@@ -1,56 +1,44 @@
|
|
|
1
1
|
import { useAtprotoSession } from "#imports";
|
|
2
2
|
import { useRouter, useRuntimeConfig } from "nuxt/app";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
-
}
|
|
9
|
-
export async function waitForAtprotoReady(nuxtApp) {
|
|
10
|
-
const deadline = Date.now() + ATPROTO_READY_TIMEOUT_MS;
|
|
11
|
-
while (Date.now() < deadline) {
|
|
12
|
-
const $atproto = nuxtApp.$atproto;
|
|
13
|
-
if ($atproto && $atproto.status.value !== "initializing") {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
await sleep(ATPROTO_READY_POLL_MS);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
loadActorDesktopStateMap,
|
|
5
|
+
resolveDesktopId
|
|
6
|
+
} from "./utilAtprotoApplicationStates.js";
|
|
19
7
|
export async function resolveRestoreActorDid(nuxtApp) {
|
|
20
8
|
const router = nuxtApp.runWithContext(() => useRouter());
|
|
21
9
|
await router.isReady();
|
|
22
10
|
const routeDid = router.currentRoute.value.params.did;
|
|
23
|
-
if (typeof routeDid === "string" && routeDid.length > 0)
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const desktop = runtimeConfig.public.desktop;
|
|
32
|
-
if (desktop.atprotoPersistence?.loadOwnerDesktopOnMounted) {
|
|
11
|
+
if (typeof routeDid === "string" && routeDid.length > 0) return routeDid;
|
|
12
|
+
if (!nuxtApp.$atproto) return void 0;
|
|
13
|
+
const { isLogged, session } = nuxtApp.runWithContext(
|
|
14
|
+
() => useAtprotoSession()
|
|
15
|
+
);
|
|
16
|
+
if (isLogged.value && session.value?.sub) return session.value.sub;
|
|
17
|
+
const desktop = nuxtApp.runWithContext(() => useRuntimeConfig()).public.desktop;
|
|
18
|
+
if (desktop.atprotoPersistence?.loadOwnerDesktopOnMounted)
|
|
33
19
|
return desktop.atprotoDesktop?.owner?.did;
|
|
34
|
-
}
|
|
35
20
|
return void 0;
|
|
36
21
|
}
|
|
37
|
-
|
|
38
|
-
async function
|
|
39
|
-
await waitForAtprotoReady(nuxtApp);
|
|
22
|
+
const loads = /* @__PURE__ */ new WeakMap();
|
|
23
|
+
export async function ensureActorStatesLoaded(nuxtApp, refresh = false) {
|
|
40
24
|
const actorDid = await resolveRestoreActorDid(nuxtApp);
|
|
41
|
-
if (!actorDid) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
25
|
+
if (!actorDid) return {};
|
|
26
|
+
const desktopId = resolveDesktopId(nuxtApp);
|
|
27
|
+
let cache = loads.get(nuxtApp);
|
|
28
|
+
if (!cache) loads.set(nuxtApp, cache = /* @__PURE__ */ new Map());
|
|
29
|
+
const key = `${desktopId}/${actorDid}`;
|
|
30
|
+
if (refresh) cache.delete(key);
|
|
31
|
+
if (!cache.has(key)) {
|
|
32
|
+
const task = loadActorDesktopStateMap(nuxtApp, actorDid, desktopId).catch(
|
|
33
|
+
(error) => {
|
|
34
|
+
cache.delete(key);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
cache.set(key, task);
|
|
49
39
|
}
|
|
40
|
+
return cache.get(key);
|
|
50
41
|
}
|
|
51
|
-
export function
|
|
52
|
-
|
|
53
|
-
actorStatesPromise = loadActorStatesOnce(nuxtApp);
|
|
54
|
-
}
|
|
55
|
-
return actorStatesPromise;
|
|
42
|
+
export function invalidateActorStates(nuxtApp) {
|
|
43
|
+
loads.delete(nuxtApp);
|
|
56
44
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Serializes remote writes per store and retries the latest value after failures. */
|
|
2
|
+
export declare function createAtprotoWriteQueue(write: (key: string, value: string) => Promise<void>, options?: {
|
|
3
|
+
retryMs?: number;
|
|
4
|
+
onError?: (error: unknown) => void;
|
|
5
|
+
}): {
|
|
6
|
+
enqueue(key: string, value: string): Promise<void> | undefined;
|
|
7
|
+
flush: () => Promise<void[]>;
|
|
8
|
+
stop(): void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function createAtprotoWriteQueue(write, options = {}) {
|
|
2
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3
|
+
const acknowledged = /* @__PURE__ */ new Map();
|
|
4
|
+
const running = /* @__PURE__ */ new Map();
|
|
5
|
+
const timers = /* @__PURE__ */ new Map();
|
|
6
|
+
let stopped = false;
|
|
7
|
+
async function drain(key) {
|
|
8
|
+
while (!stopped && pending.has(key)) {
|
|
9
|
+
const value = pending.get(key);
|
|
10
|
+
try {
|
|
11
|
+
await write(key, value);
|
|
12
|
+
acknowledged.set(key, value);
|
|
13
|
+
if (pending.get(key) === value) pending.delete(key);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
options.onError?.(error);
|
|
16
|
+
if (!stopped && !timers.has(key)) {
|
|
17
|
+
timers.set(
|
|
18
|
+
key,
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
timers.delete(key);
|
|
21
|
+
void flush(key);
|
|
22
|
+
}, options.retryMs ?? 5e3)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function flush(key) {
|
|
30
|
+
if (stopped) return Promise.resolve();
|
|
31
|
+
const active = running.get(key);
|
|
32
|
+
if (active) return active;
|
|
33
|
+
const timer = timers.get(key);
|
|
34
|
+
if (timer) clearTimeout(timer);
|
|
35
|
+
timers.delete(key);
|
|
36
|
+
const task = drain(key).finally(() => {
|
|
37
|
+
running.delete(key);
|
|
38
|
+
});
|
|
39
|
+
running.set(key, task);
|
|
40
|
+
return task;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
enqueue(key, value) {
|
|
44
|
+
if (stopped) return;
|
|
45
|
+
if (!pending.has(key) && acknowledged.get(key) === value) return;
|
|
46
|
+
pending.set(key, value);
|
|
47
|
+
return flush(key);
|
|
48
|
+
},
|
|
49
|
+
flush: () => Promise.all([...pending.keys()].map(flush)),
|
|
50
|
+
stop() {
|
|
51
|
+
stopped = true;
|
|
52
|
+
for (const timer of timers.values()) clearTimeout(timer);
|
|
53
|
+
timers.clear();
|
|
54
|
+
pending.clear();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owdproject/nuxt-module-atproto-persistence",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
3
|
+
"version": "4.0.0-alpha.4",
|
|
4
4
|
"description": "Pinia persistence via AT Protocol repository, ESM-friendly",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/atproto-os/module-atproto-persistence",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"dev:build": "nuxt build playground",
|
|
38
38
|
"dev:generate": "NUXT_APP_BASE_URL=${NUXT_APP_BASE_URL:-/module-atproto-persistence/} nuxt generate playground",
|
|
39
39
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && cd playground && pnpm exec nuxt prepare",
|
|
40
|
-
"validate": "desktop validate ."
|
|
40
|
+
"validate": "desktop validate .",
|
|
41
|
+
"test:persistence": "vitest run"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@nuxt/kit": "^4.6.2",
|
|
@@ -51,13 +52,16 @@
|
|
|
51
52
|
"@owdproject/core": "^4.0.0-alpha.3",
|
|
52
53
|
"nuxt": "^4.6.2",
|
|
53
54
|
"pinia": "^3.0.0",
|
|
54
|
-
"typescript": "~6.0.3"
|
|
55
|
+
"typescript": "~6.0.3",
|
|
56
|
+
"vitest": "^4.1.12",
|
|
57
|
+
"vue": "^3.5.44"
|
|
55
58
|
},
|
|
56
59
|
"peerDependencies": {
|
|
57
60
|
"@owdproject/core": "^4.0.0-alpha.3",
|
|
58
61
|
"@owdproject/nuxt-module-atproto": "^4.0.0-alpha.3",
|
|
59
|
-
"pinia": "^3.0.0",
|
|
60
|
-
"@owdproject/nuxt": "^4.0.0-alpha.3"
|
|
62
|
+
"pinia": "^3.0.0 || ^4.0.0",
|
|
63
|
+
"@owdproject/nuxt": "^4.0.0-alpha.3",
|
|
64
|
+
"vue": ">=3.3.0"
|
|
61
65
|
},
|
|
62
66
|
"publishConfig": {
|
|
63
67
|
"access": "public",
|