Pinia Persistent Storage via AT Protocol for Open Web Desktop

chrome: Fix linting and formatting

dxlliv 53eece10 129aa17e

+210 -103
+1
README.md
··· 12 12 for storing Open Web Desktop states persistently on the AT Protocol. 13 13 14 14 ## Features 15 + 15 16 - Configures `pinia-plugin-persistedstate-2` to use `atproto` 16 17 - Enables persistent storage for Pinia stores 17 18 - Works seamlessly with Nuxt
+55 -103
runtime/plugin.ts
··· 1 - import {createPersistedStatePlugin} from 'pinia-plugin-persistedstate-2' 2 - import {useOnline} from '@vueuse/core' 3 - import {deepEqual} from "@owdproject/core/runtime/utils/utilCommon"; 4 - import {defineNuxtPlugin, useNuxtApp} from "nuxt/app" 5 - import {toRaw} from "vue" 6 - import {useAtproto} from "#imports" 7 - import {usePinia} from "#imports" 1 + import { createPersistedStatePlugin } from 'pinia-plugin-persistedstate-2' 2 + import { deepEqual } from '@owdproject/core/runtime/utils/utilCommon' 3 + import { defineNuxtPlugin, useRuntimeConfig } from 'nuxt/app' 4 + import { toRaw } from 'vue' 5 + import { usePinia, useAtproto } from '#imports' 6 + 8 7 import localforage from 'localforage/src/localforage.js' 9 8 import { 10 - getAtprotoApplicationState, 11 - putAtprotoApplicationState, 12 - listAtprotoApplicationStateRecords, parseAtprotoStoreKey 13 - } from "./utils/utilAtprotoApplications"; 14 - 15 - function shouldSyncWithATProto( 16 - piniaStoreKey: string, 17 - atprotoApplicationsRecords?: { 18 - windows: any[], 19 - meta: any[], 20 - } 21 - ) { 22 - const online = useOnline(); 23 - const {$atproto} = useNuxtApp(); 24 - 25 - if (!online.value || !$atproto.session.value) return false; 26 - 27 - const parsed = parseAtprotoStoreKey(piniaStoreKey); 28 - 29 - if (!parsed) return false; 30 - 31 - const { collection, rkey } = parsed; 32 - 33 - if (collection === 'org.owdproject.application.windows' && atprotoApplicationsRecords?.windows) { 34 - return atprotoApplicationsRecords.windows.some(record => record.uri.endsWith(rkey)); 35 - } 36 - 37 - if (collection === 'org.owdproject.application.meta' && atprotoApplicationsRecords?.meta) { 38 - return atprotoApplicationsRecords.meta.some(record => record.uri.endsWith(rkey)); 39 - } 40 - 41 - return true; 42 - } 9 + loadActorDesktop, 10 + putAtprotoApplicationState, 11 + parseAtprotoStoreKey, 12 + } from './utils/utilAtprotoApplicationStates' 43 13 44 14 export default defineNuxtPlugin({ 45 - name: 'owd-plugin-pinia-atproto', 46 - dependsOn: ['atproto', 'owd-plugin-atproto'], 47 - async setup() { 48 - const pinia = usePinia() 49 - const atproto = useAtproto() 50 - 51 - const atprotoApplicationsRecords = { 52 - windows: atproto.agent.account 53 - ? await listAtprotoApplicationStateRecords(atproto.agent.account, 'org.owdproject.application.windows') 54 - : [], 55 - meta: atproto.agent.account 56 - ? await listAtprotoApplicationStateRecords(atproto.agent.account, 'org.owdproject.application.meta') 57 - : [] 58 - } 59 - 60 - pinia.use( 61 - createPersistedStatePlugin({ 62 - persist: false, 63 - storage: { 64 - getItem: async (piniaKey) => { 65 - const piniaValue = await localforage.getItem(piniaKey); 66 - const parsed = parseAtprotoStoreKey(piniaKey); 67 - 68 - if (!parsed || !shouldSyncWithATProto(piniaKey, atprotoApplicationsRecords)) { 69 - return piniaValue; 70 - } 71 - 72 - const { collection, rkey } = parsed; 73 - 74 - return getAtprotoApplicationState(atproto.agent.account, collection, rkey) 75 - .then((response) => JSON.stringify(response.data.value)) 76 - .catch(() => piniaValue); 77 - }, 78 - setItem: async (piniaKey, piniaValue) => { 79 - const previousPiniaValue = await localforage.getItem(piniaKey); 80 - await localforage.setItem(piniaKey, piniaValue); 15 + name: 'owd-plugin-atproto-persistence', 16 + dependsOn: ['owd-plugin-atproto'], 17 + async setup(nuxt) { 18 + const pinia = usePinia() 19 + const atproto = useAtproto() 20 + const runtimeConfig = useRuntimeConfig() 81 21 82 - const parsed = parseAtprotoStoreKey(piniaKey); 22 + if ( 23 + runtimeConfig.public.atprotoPersistence && 24 + runtimeConfig.public.atprotoPersistence.loadOwnerDesktopOnMounted 25 + ) { 26 + loadActorDesktop(runtimeConfig.public.atprotoDesktop.owner.did) 27 + } 83 28 84 - if (!parsed) { 85 - return piniaValue; 86 - } 29 + pinia.use( 30 + createPersistedStatePlugin({ 31 + persist: false, 32 + storage: { 33 + getItem: async (piniaKey) => { 34 + return localforage.getItem(piniaKey) 35 + }, 36 + setItem: async (piniaKey, piniaValue) => { 37 + const previousPiniaValue = await localforage.getItem(piniaKey) 38 + await localforage.setItem(piniaKey, piniaValue) 87 39 88 - const { collection, rkey } = parsed; 40 + const atprotoTargetRecord = parseAtprotoStoreKey(piniaKey) 89 41 90 - if (deepEqual(toRaw(piniaValue), toRaw(previousPiniaValue))) { 91 - return piniaValue; 92 - } 42 + if (!atprotoTargetRecord || !atproto.agent.account) { 43 + return piniaValue 44 + } 93 45 94 - return putAtprotoApplicationState( 95 - atproto.agent.account, 96 - collection, 97 - rkey, 98 - JSON.parse(piniaValue) 99 - ); 100 - }, 101 - removeItem: async (key) => { 102 - const online = useOnline() 103 - await localforage.removeItem(key) 46 + const { collection, rkey } = atprotoTargetRecord 104 47 105 - if (!key.startsWith('owd/')) { 106 - return 107 - } 48 + if (deepEqual(toRaw(piniaValue), toRaw(previousPiniaValue))) { 49 + return piniaValue 50 + } 108 51 109 - return 110 - }, 111 - }, 112 - }), 113 - ) 114 - } 52 + return putAtprotoApplicationState( 53 + atproto.agent.account, 54 + atproto.agent.account.assertDid, 55 + collection, 56 + rkey, 57 + JSON.parse(piniaValue), 58 + ) 59 + }, 60 + removeItem: async (key) => { 61 + await localforage.removeItem(key) 62 + }, 63 + }, 64 + }), 65 + ) 66 + }, 115 67 })
+154
runtime/utils/utilAtprotoApplicationStates.ts
··· 1 + import { useAgent, resolveActorServiceEndpoint } from '#imports' 2 + import { useDesktopStore } from '@owdproject/core/runtime/stores/storeDesktop' 3 + import { useApplicationWindowsStore } from '@owdproject/core/runtime/stores/storeApplicationWindows' 4 + import { useApplicationMetaStore } from '@owdproject/core/runtime/stores/storeApplicationMeta' 5 + 6 + export function parseAtprotoStoreKey( 7 + key: string, 8 + ): { collection: string; rkey: string } | null { 9 + if (!key.startsWith('owd/')) return null 10 + 11 + const parts = key.split('/') 12 + 13 + if (parts[1] === 'application') { 14 + const last = parts[parts.length - 1] 15 + if (last === 'windows' || last === 'meta') { 16 + const nome = parts.slice(2, -1).join('/') 17 + return { 18 + collection: `org.owdproject.application.${last}`, 19 + rkey: nome, 20 + } 21 + } 22 + } 23 + 24 + // owd/desktop 25 + if (parts[1] === 'desktop') { 26 + const rkey = parts[2] ?? 'self' 27 + return { 28 + collection: 'org.owdproject.desktop', 29 + rkey, 30 + } 31 + } 32 + 33 + return null 34 + } 35 + 36 + export function listAtprotoApplicationStateRecords( 37 + agent: any, 38 + repo: string, 39 + collection: string, 40 + ) { 41 + return agent.com.atproto.repo 42 + .listRecords({ 43 + repo, 44 + collection, 45 + }) 46 + .then((response: any) => response.data) 47 + .then((data: any) => data.records) 48 + .catch(() => []) 49 + } 50 + 51 + export function getAtprotoApplicationState( 52 + agent: any, 53 + repo: string, 54 + collection: string, 55 + rkey: string, 56 + ) { 57 + return agent.com.atproto.repo.getRecord({ 58 + repo, 59 + collection, 60 + rkey, 61 + }) 62 + } 63 + 64 + export function putAtprotoApplicationState( 65 + agent: any, 66 + collection: string, 67 + rkey: string, 68 + record: any, 69 + ) { 70 + return agent.com.atproto.repo.putRecord({ 71 + repo: agent?.assertDid as string, 72 + collection, 73 + rkey, 74 + record, 75 + }) 76 + } 77 + 78 + /** 79 + * Load actor desktop 80 + * 81 + * @param actorDid 82 + */ 83 + export async function loadActorDesktop(actorDid: string) { 84 + const actorServiceEndpoint = await resolveActorServiceEndpoint(actorDid) 85 + const actorAgent = useAgent(actorServiceEndpoint) 86 + 87 + const [ 88 + atprotoActorDesktopRecord, 89 + atprotoApplicationWindowsList, 90 + atprotoApplicationMetaList, 91 + ] = await Promise.allSettled([ 92 + getAtprotoApplicationState( 93 + actorAgent, 94 + actorDid, 95 + 'org.owdproject.desktop', 96 + 'self', 97 + ), 98 + listAtprotoApplicationStateRecords( 99 + actorAgent, 100 + actorDid, 101 + 'org.owdproject.application.windows', 102 + ), 103 + listAtprotoApplicationStateRecords( 104 + actorAgent, 105 + actorDid, 106 + 'org.owdproject.application.meta', 107 + ), 108 + ]) 109 + 110 + // load actor applications desktop settings 111 + if (atprotoActorDesktopRecord.status === 'fulfilled') { 112 + // todo validate 113 + 114 + if ( 115 + atprotoActorDesktopRecord.value && 116 + atprotoActorDesktopRecord.value.data 117 + ) { 118 + useDesktopStore().$patch(atprotoActorDesktopRecord.value.data.value.state) 119 + } 120 + } 121 + 122 + let applicationWindowsStore 123 + let applicationMetaStore 124 + 125 + // load actor applications windows 126 + if (atprotoApplicationWindowsList.status === 'fulfilled') { 127 + for (const atprotoApplicationWindowRecord of atprotoApplicationWindowsList.value) { 128 + const atprotoApplicationId = atprotoApplicationWindowRecord.uri 129 + .split('/') 130 + .pop() 131 + 132 + applicationWindowsStore = useApplicationWindowsStore(atprotoApplicationId) 133 + 134 + applicationWindowsStore.$patch({ 135 + windows: atprotoApplicationWindowRecord.value.windows, 136 + }) 137 + } 138 + } 139 + 140 + // load actor applications meta 141 + if (atprotoApplicationMetaList.status === 'fulfilled') { 142 + for (const atprotoApplicationMetaRecord of atprotoApplicationMetaList.value) { 143 + const atprotoApplicationId = atprotoApplicationMetaRecord.uri 144 + .split('/') 145 + .pop() 146 + 147 + applicationMetaStore = useApplicationMetaStore(atprotoApplicationId)() 148 + 149 + applicationMetaStore.$patch({ 150 + ...atprotoApplicationMetaRecord.value, 151 + }) 152 + } 153 + } 154 + }