diff --git a/.env.example b/.env.example deleted file mode 100644 index dc58fb6..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -DATABASE="file:///tmp/tokyo.db" -TURSO_AUTH_TOKEN="" diff --git a/.mise.toml b/.mise.toml index bdccd62..314ff4f 100644 --- a/.mise.toml +++ b/.mise.toml @@ -3,3 +3,6 @@ bun = "latest" task = "3.32" protoc = "21.12" rust = "latest" + +[env] +RUST_BACKTRACE = "1" diff --git a/Taskfile.yml b/Taskfile.yml index 68f7fa2..c805199 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,8 +2,6 @@ version: "3" -dotenv: [".env"] - includes: server: dir: apps/server @@ -75,6 +73,9 @@ tasks: dev: desc: Run desktop app with server + env: + DATABASE: "file:///tmp/tokyo.db" + TURSO_AUTH_TOKEN: "" deps: [setup] cmds: - task --parallel server:dev app:dev diff --git a/packages/accessors/src/adapters/solid.ts b/packages/accessors/src/adapters/solid.ts index b957bc3..7a2dbc3 100644 --- a/packages/accessors/src/adapters/solid.ts +++ b/packages/accessors/src/adapters/solid.ts @@ -7,33 +7,39 @@ import { createEffect, createSignal } from 'solid-js'; * @param params The params as a signal, that will be used to fetch and filter the data. */ -export function useAccessor>(accessorFn: () => T) { +export function useAccessor>(accessorFn: () => T) { const accessor = accessorFn(); const [data, setData] = createSignal< - Awaited> | undefined + Awaited> | undefined >(); - const [error, setError] = createSignal(); + + type Query = Partial<(typeof accessor)['query']>; + type Params = Partial<(typeof accessor)['params']>; + const [pending, setPending] = createSignal(); - const [params, setParams] = createSignal>(); + const [params, setParams] = createSignal(); + const [query, setQuery] = createSignal(); - accessor.on('data', (data) => { - setData(data); - }); - accessor.on('error', (error) => setError(error)); + accessor.on('data', (data) => setData(data)); accessor.on('pending', (pending) => setPending(pending)); createEffect(() => { - if (params) { - accessor.setParams(params()); - } + accessor.params = params(); + }); + + createEffect(() => { + accessor.query = query(); }); return { data, - error, pending, - params(p?: ReturnType) { - if (p) setParams(p); + query(value?: Query) { + if (value) setQuery(value); + return query(); + }, + params(value?: Params) { + if (value) setParams(value); return params(); }, }; diff --git a/packages/accessors/src/lib.ts b/packages/accessors/src/lib.ts index faedcbd..7dc1c86 100644 --- a/packages/accessors/src/lib.ts +++ b/packages/accessors/src/lib.ts @@ -1,126 +1,134 @@ import * as Comlink from 'comlink'; -export type AccessorParams = { - /** - * The query parameters that will be used to fetch the data. - */ - query: Record; +/** + * [x] Create request message(s) from params. + * [x] Handle responses and associate them to the correct request. + * [x] Cache data by specific request params for multiple requests. + * [x] Request creation can check the cached params for overlap and exclude already cached data from the request. (outside) + * [x] Filter or transform cached data before returning it to the user. + * [x] Invalidate cache on mutation (and when outdated (~1h old) -- not implemented) + * [x] Abort signaling (ignore responses from aborted requests) + * [x] Internal message types, fetch states, progress + * [x] Accessors can be chained for a layered caching approach -- if the query changes before the request is finished, abort (ignore incoming response) the request and start a new one; only handle responses the have a nonce included in cacheKeys + * [ ] TODO: Handle compute errors + * [ ] TODO: Clear cache entries that were not accessed for a while to save memory + * [ ] TODO: Stream params to the accessor + */ - mutation?: Record; +const CACHE_MAX_AGE = 1000 * 60 * 60; // 1h +// biome-ignore lint/suspicious/noExplicitAny: +const CACHE = new Map(); +const CACHE_TIMESTAMPS = new Map(); + +class MultiplexStream extends WritableStream { + constructor(write: WritableStream) { + super({ + write: async (msg) => { + const writer = write.getWriter(); + writer.write(msg); + writer.releaseLock(); + }, + }); + } +} - [key: string]: any; +type AccessorState = { + progress?: number; + message?: string; }; /** - * This class is responsible for handling the communication with the API, caching that data and keeping the data up to date with the given parameters. + * Responsible for handling the communication with the API, caching that data and keeping the data up to date with the given parameters. */ -export class Accessor { - public params?: Params; - - public error?: Error; +export class Accessor< + Query, + Params, + HandledMessage, + Data, + RequestMessage extends { nonce?: string }, + ResponseMessage extends { nonce?: string; type?: string; state?: AccessorState }, +> { + private _query?: Query; + private _params?: Params; + + public state: AccessorState = {}; + + public get query() { + return this._query; + } - /** - * Indicates if the accessor is currently fetching or filtering data. - */ - public pending = false; + public get params() { + return this._params; + } - private _cacheKey: string[] = []; - private _cache: (Cache | undefined)[] = []; + private cacheKeys: string[] = []; /** - * Set the params for this accessor. This will trigger a new request to the API when needed. + * Set the query for this accessor. This will trigger a new request to the API for an invalid cache. */ - public setParams(params: Params) { - const req = this._strategy.createRequest(params, this._cache); + public set query(query: Query | undefined) { + // shallow check, skip if queries are the same + if (!query || query === this._query) return; - if (Array.isArray(req)) { - throw new Error('Multiple requests are not supported yet'); - } + const requests = this._strategy.createRequest(query); + if (requests === undefined) return; - const messageId = 0; - if (req) { - const cacheKey = JSON.stringify(req); + this._query = query; - if (cacheKey !== this._cacheKey[messageId]) { - this.params = params; - this._cacheKey[messageId] = cacheKey; - this._cache[messageId] = undefined; + const now = Date.now(); - this.setPending(true); + let cached = true; - req._nonce = messageId; + for (const req of requests) { + const id = requests.indexOf(req); - const writer = this.tx.getWriter(); - writer.write(req); - writer.releaseLock(); + const cacheKey = `${id}.${JSON.stringify(requests)}`; + this.cacheKeys[id] = cacheKey; - this.dispatch('request', params); - } else if (this._cache[messageId] && params !== this.params) { - this.params = params; + const timestamp = CACHE_TIMESTAMPS.get(cacheKey); + const age = timestamp ? now - timestamp : now; - this.dispatch('data', this.processData()); - this.setPending(false); + if (CACHE.has(cacheKey) && age <= CACHE_MAX_AGE) { + // skip request if cache is valid + continue; } - } - this.params = params; - this.clearError(); - } - - public mutate(mutation: any) { - const writer = this.tx.getWriter(); - writer.write(mutation); - writer.releaseLock(); - } + cached = false; - public write = new WritableStream({ - write: (msg) => { - console.log(msg); + CACHE.delete(cacheKey); + CACHE_TIMESTAMPS.delete(cacheKey); - // this.setParams(msg); - }, - }); + req.nonce = cacheKey; + this.request(req); + } - /** - * Returns the filtered data if available. - */ - public processData() { - if (this._strategy.filter) { - return this._strategy.filter(this._cache, this.params); + if (cached) { + this.compute(); } } - private target = new EventTarget(); + /** + * Set the params for this accessor. This will only trigger a recompute, never a new request. + */ + public set params(params: Params | undefined) { + // shallow check, skip if params are the same + if (!params || params === this._params) return; + this._params = params; - // public on(event: 'data', callback: (payload?: Data) => void): () => void; - // public on(event: 'pending', callback: (payload?: undefined) => void): () => void; - // public on(event: 'error', callback: (payload?: undefined) => void): () => void; - public on(event: 'data' | 'pending' | 'error' | 'request', callback: (payload?: any) => void) { - const listener = ((ev: CustomEvent) => callback(ev.detail)) as EventListener; + this.compute(); + } - this.target.addEventListener(event, listener); + private _pending = false; - if ('WorkerGlobalScope' in globalThis) { - return Comlink.proxy(() => { - this.target.removeEventListener(event, listener); - }); - } - return () => { - this.target.removeEventListener(event, listener); - }; + private set pending(pending: boolean) { + this._pending = pending; + this.emit('pending'); } - private dispatch(event: 'data', payload?: Data): void; - private dispatch(event: 'request', payload?: Params): void; - private dispatch(event: 'pending', payload?: undefined): void; - private dispatch(event: 'error', payload?: undefined): void; - private dispatch(event: string, payload?: undefined) { - this.target.dispatchEvent(new CustomEvent(event, { detail: payload })); + public get pending() { + return this._pending; } - private rx: ReadableStream; - private tx: WritableStream; - constructor( clients: { stream(): readonly [ReadableStream, WritableStream]; @@ -129,83 +137,105 @@ export class Accessor Data; + compute: (data: (HandledMessage | undefined)[], params: Params | undefined) => Data; } ) { // If a client connects after a request has been made, the request will run into the void. // Not a concern here, since we connect in the constructor. - const [read, write] = clients[0].stream(); // TODO: use all clients in the array - this.rx = read; - this.tx = write; - - this.rx - ?.pipeThrough( - new TransformStream({ - /** - * Handle response. - */ - transform: async (msg, controller) => { - if (msg._type === 'error') { - console.error('ohno an error, this should be handled!', msg); - - if (msg.error) { - this.onError(msg.error); - } - } else { - const res = await this._strategy.handleMessage(msg, this.params); - if (res) controller.enqueue(res); - } - }, - }) - ) - ?.pipeThrough( - new TransformStream({ - /** - * Cache the prepared data from the api. - */ - transform: async (msg, controller) => { - const messageId = msg._nonce ? msg._nonce : 0; - this._cache[messageId] = msg; - controller.enqueue(msg); - }, - }) - ) - .pipeTo( - new WritableStream({ - /** - * Handles responses from the api. - */ - write: (msg) => { - this.dispatch('data', this.processData()); - this.setPending(false); - }, - }) - ); + for (const client of clients) { + this.stream(client); + } } - private clearError() { - this.error = undefined; + /** + * Returns the filtered data if available. + */ + public compute() { + const data: (HandledMessage | undefined)[] = []; + + for (const key of this.cacheKeys) { + data.push(CACHE.get(key)); + } + + this.emit('data', this._strategy.compute(data, this.params)); + this.pending = false; } - private onError(err: Error) { - this.error = err; - this.dispatch('error'); - this.setPending(false); + private receive = new WritableStream({ + write: async (msg) => { + if (msg.type === 'state') { + this.state = Object.assign(this.state, msg.state); + this.emit('state', this.state); + return; + } + + const nonce = msg.nonce; // corresponds to the request message + + if (nonce && this.cacheKeys.includes(nonce)) { + const data = await this._strategy.transform(msg); + + CACHE.set(nonce, data); + CACHE_TIMESTAMPS.set(nonce, Date.now()); + + this.compute(); + } + }, + }); + + private streams: WritableStream[] = []; + + private stream(client: { + stream(): readonly [ReadableStream, WritableStream]; + }) { + const [read, write] = client.stream(); + this.streams.push(write); + read.pipeTo(new MultiplexStream(this.receive)); } - private setPending(pending: boolean) { - this.pending = pending; - this.dispatch('pending'); + public request(mutation: any) { + this.pending = true; + + // replicate the request to all peers + for (const stream of this.streams) { + const writer = stream.getWriter(); + writer.write(mutation); + writer.releaseLock(); + } + } + + private target = new EventTarget(); + + public on(event: 'data', callback: (payload?: Data) => void): () => void; + public on(event: 'pending', callback: (payload?: undefined) => void): () => void; + public on(event: 'error', callback: (payload?: undefined) => void): () => void; + public on(event: 'state', callback: (payload?: AccessorState) => void): () => void; + public on(event: string, callback: (payload?: undefined) => void) { + const listener = ((ev: CustomEvent) => callback(ev.detail)) as EventListener; + + this.target.addEventListener(event, listener); + + if ('WorkerGlobalScope' in globalThis) { + return Comlink.proxy(() => { + this.target.removeEventListener(event, listener); + }); + } + return () => { + this.target.removeEventListener(event, listener); + }; + } + + private emit(event: 'state', payload?: AccessorState): void; + private emit(event: 'data', payload?: Data): void; + private emit(event: 'pending', payload?: undefined): void; + private emit(event: string, payload?: undefined) { + this.target.dispatchEvent(new CustomEvent(event, { detail: payload })); } } diff --git a/packages/api/src/Worker.ts b/packages/api/src/Worker.ts index 6b54fb1..af4c635 100644 --- a/packages/api/src/Worker.ts +++ b/packages/api/src/Worker.ts @@ -1,14 +1,16 @@ +import * as Comlink from 'comlink'; +import * as library from 'tokyo-proto'; import RemoteLibrary from './api/RemoteLibrary.ts?worker'; -import * as Comlink from 'comlink'; -import { MessageType } from './lib.ts'; +type Remote = typeof import('./api/RemoteLibrary.ts').default; +type Message = ReturnType; export default { stream() { const url = '127.0.0.1:8000/ws'; const worker = new RemoteLibrary(); - const wrappedWorker = Comlink.wrap(worker); + const wrappedWorker = Comlink.wrap(worker); worker.onerror = (err) => { console.error('Error in worker:', err); @@ -16,19 +18,17 @@ export default { wrappedWorker.connect(url); - const read = new ReadableStream<{ _type: MessageType }>({ + const read = new ReadableStream({ start(ctlr) { wrappedWorker.onMessage( Comlink.proxy((msg) => { - console.log(msg); - ctlr.enqueue(msg); }) ); }, }); - const write = new WritableStream({ + const write = new WritableStream({ write(chunk) { wrappedWorker.send(chunk); }, diff --git a/packages/api/src/accessors/image.ts b/packages/api/src/accessors/image.ts new file mode 100644 index 0000000..44083dc --- /dev/null +++ b/packages/api/src/accessors/image.ts @@ -0,0 +1,24 @@ +import { MessageType } from '../lib.js'; +import { Accessor } from 'tokyo-accessors'; +import Worker from '../Worker.js'; +import * as proto from 'tokyo-proto'; + +export function createImageAccessor() { + return new Accessor([Worker], { + createRequest(query: unknown) { + return [ + proto.ClientMessage.create({ + // locations: proto.RequestLocations.create({}), + }), + ]; + }, + + transform(msg) { + if (msg.type === MessageType.Locations) return msg; + }, + + compute([data]) { + return data?.data; + }, + }); +} diff --git a/packages/api/src/accessors/index.ts b/packages/api/src/accessors/index.ts index fbf71b9..4df8421 100644 --- a/packages/api/src/accessors/index.ts +++ b/packages/api/src/accessors/index.ts @@ -6,33 +6,35 @@ import * as proto from 'tokyo-proto'; export function createIndexAccessor() { return new Accessor([Worker], { - createRequest(params: { - query: { - locations: string[]; - }; - filterRating: number; - sortRating: boolean; - sortCreated: boolean; + createRequest(query: { + locations: string[]; }) { - if (params?.query) { - return proto.ClientMessage.create({ + return [ + proto.ClientMessage.create({ index: proto.RequestLibraryIndex.create({ - ids: params.query.locations, + ids: query.locations, }), - }); - // return { - // _type: MessageType.Index, - // locations: params.query.locations, - // }; - } + }), + ]; + // return { + // _type: MessageType.Index, + // locations: params.query.locations, + // }; }, - handleMessage(msg) { - if (msg._type === MessageType.Index) return msg; + transform(msg) { + if (msg.type === MessageType.Index) return msg; }, - filter: ([data], params) => { - const items = data.data.index; + compute: ( + [data], + params?: { + filterRating: number; + sortRating: boolean; + sortCreated: boolean; + } + ) => { + const items = data?.data.index || []; const sort = { rating: (a: IndexEntryMessage, b: IndexEntryMessage) => { diff --git a/packages/api/src/accessors/locations.ts b/packages/api/src/accessors/locations.ts index 934382a..32b4a7f 100644 --- a/packages/api/src/accessors/locations.ts +++ b/packages/api/src/accessors/locations.ts @@ -5,12 +5,12 @@ import * as proto from 'tokyo-proto'; export function createLocationsAccessor() { return new Accessor([Worker], { - createRequest(params: { - query: unknown; - }) { - return proto.ClientMessage.create({ - locations: proto.RequestLocations.create({}), - }); + createRequest(query: unknown) { + return [ + proto.ClientMessage.create({ + locations: proto.RequestLocations.create({}), + }), + ]; // Create a library // library.ClientMessage.create({ // create: library.CreateLibraryMessage.create({ @@ -20,12 +20,12 @@ export function createLocationsAccessor() { // }) }, - handleMessage(msg) { - if (msg._type === MessageType.Locations) return msg; + transform(msg) { + if (msg.type === MessageType.Locations) return msg; }, - filter: ([data]) => { - return data.data; + compute([data]) { + return data?.data; }, }); } diff --git a/packages/api/src/accessors/metadata.ts b/packages/api/src/accessors/metadata.ts index 28d70bd..ba5469f 100644 --- a/packages/api/src/accessors/metadata.ts +++ b/packages/api/src/accessors/metadata.ts @@ -29,28 +29,21 @@ export function createMetadataAccessor() { }; return new Accessor([Worker], { - createRequest( - params: { - query: { - ids: string[]; - }; - }, - cache - ) { - if (params?.query) { - // const ids = params.query.ids?.filter((id) => !cache[0]?.find((entry) => entry.id === id)); - const ids = params.query.ids; - - return proto.ClientMessage.create({ + createRequest(query: { + ids: string[]; + }) { + // const ids = params.query.ids?.filter((id) => !cache[0]?.find((entry) => entry.id === id)); + return [ + proto.ClientMessage.create({ meta: proto.RequestMetadata.create({ - file: ids, + file: query.ids, }), - }); - } + }), + ]; }, - async handleMessage(msg) { - if (msg._type === MessageType.Metadata) { + async transform(msg) { + if (msg.type === MessageType.Metadata) { const entry = async (entry) => { const buff = new Uint8Array(entry.thumbnail); const blob = new Blob([buff]); @@ -67,6 +60,6 @@ export function createMetadataAccessor() { } }, - filter: ([data]) => data, + compute: ([data]) => data, }); } diff --git a/packages/api/src/accessors/thumbnails.ts b/packages/api/src/accessors/thumbnails.ts index 210d238..c8cb0bb 100644 --- a/packages/api/src/accessors/thumbnails.ts +++ b/packages/api/src/accessors/thumbnails.ts @@ -24,20 +24,19 @@ export function createThumbnailAccessor(hosts: string[]) { }; return new Accessor([Worker], { - createRequest(params: { - query: { - ids: string[]; - }; + createRequest(query: { + ids: string[]; }) { - if (params.query) - return { - _type: MessageType.Thumbnails, - ids: params.query.ids, - }; + return [ + { + type: MessageType.Thumbnails, + ids: query.ids, + }, + ]; }, - handleMessage(msg) { - if (msg._type === MessageType.Locations) { + transform(msg) { + if (msg.type === MessageType.Locations) { const thumbs = msg.data.map((d) => { const buff = new Uint8Array(d.data.thumbnail); const blob = new Blob([buff]); @@ -47,6 +46,6 @@ export function createThumbnailAccessor(hosts: string[]) { } }, - filter: ([data]) => data, + compute: ([data]) => data, }); } diff --git a/packages/api/src/api/RemoteLibrary.ts b/packages/api/src/api/RemoteLibrary.ts index 10bdf9e..e543ef9 100644 --- a/packages/api/src/api/RemoteLibrary.ts +++ b/packages/api/src/api/RemoteLibrary.ts @@ -16,36 +16,40 @@ export class RemoteLibrary { messageListeners = new Set<(arg: library.Message) => void>(); - public async onMessage(callback: (arg: any) => void, id?: number) { + public async onMessage(callback: (arg: any) => void) { const listener = async (msg: library.Message) => { - if (id !== undefined && id !== msg.id) { - return; - } + callback(this.parseMessage(msg)); + }; + this.messageListeners.add(listener); + return () => this.messageListeners.delete(listener); + } + + private emit(message: any) { + for (const listener of this.messageListeners) { + listener(message); + } + } - for (const key in msg) { - if (msg[key] !== undefined) { - callback({ - _type: messageKeyToType[key] || key, - data: msg[key], - }); - return; - } + parseMessage(msg: library.Message) { + for (const key in msg) { + if (key !== 'nonce' && msg[key] !== undefined) { + return { + type: messageKeyToType[key] || key, + nonce: msg.nonce, + data: msg[key], + }; } + } - callback({ - _type: MessageType.Error, - message: 'Message not handled', - }); + return { + type: MessageType.Error, + message: 'Message not handled', }; - this.messageListeners.add(listener); - return () => this.messageListeners.delete(listener); } backlog: library.ClientMessage[] = []; send(msg: library.ClientMessage) { - console.log('RemoteLibrary.send', msg); - if (this.ws.readyState !== this.ws.OPEN) { this.backlog.push(msg); } else { @@ -73,8 +77,6 @@ export class RemoteLibrary { connect(host: string) { console.log('worker connecting to', host); - const self = this; - const ws = new WebSocket(`ws://${host}`); ws.onopen = () => { @@ -94,6 +96,7 @@ export class RemoteLibrary { }, }); + // TODO: should transfer (Comlink.transfer) a stream instead of a event listener rx.pipeThrough( new TransformStream({ async transform(msg, controller) { @@ -110,10 +113,8 @@ export class RemoteLibrary { }) ).pipeTo( new WritableStream({ - write(message) { - for (const listener of self.messageListeners) { - listener(message); - } + write: (message) => { + this.emit(message); }, }) ); diff --git a/packages/api/src/lib.ts b/packages/api/src/lib.ts index a1f1d18..c0074b6 100644 --- a/packages/api/src/lib.ts +++ b/packages/api/src/lib.ts @@ -46,3 +46,4 @@ export { createIndexAccessor } from '../src/accessors/index.ts'; export { createLocationsAccessor } from '../src/accessors/locations.ts'; export { createMetadataAccessor } from '../src/accessors/metadata.ts'; export { createThumbnailAccessor } from '../src/accessors/thumbnails.ts'; +export { createImageAccessor } from '../src/accessors/image.ts'; diff --git a/packages/library/src/ws.rs b/packages/library/src/ws.rs index 4cd4273..66f33eb 100644 --- a/packages/library/src/ws.rs +++ b/packages/library/src/ws.rs @@ -97,30 +97,31 @@ async fn get_index_msg(lib: &Library, ids: Vec) -> schema::LibraryIndexM return index_msg; } -async fn handle_socket_message(msg: ClientMessage) -> Result { +async fn handle_socket_message(req: ClientMessage) -> Result { let lib = &Library::new().await; - if msg.has_locations() { + if req.has_locations() { let mut msg = schema::Message::new(); - msg.id = msg.id; + msg.nonce = req.nonce; msg.set_list(get_location_list(lib).await); let packet = ws::Message::Binary(msg.write_to_bytes().unwrap()); return Ok(packet); } - if msg.has_index() { - let index = msg.index(); - println!("Requested Index {:?}", index); + if req.has_index() { let mut msg = schema::Message::new(); - msg.id = msg.id; + msg.nonce = req.nonce.clone(); + + let index = req.index(); + println!("Requested Index {:?}", index); msg.set_index(get_index_msg(lib, index.ids.clone()).await); let bytes = msg.write_to_bytes().unwrap(); let packet = ws::Message::Binary(bytes); return Ok(packet); } - if msg.has_create() { - let create = msg.create(); + if req.has_create() { + let create = req.create(); let _cr = lib .create_library(create.name.to_string(), create.path.to_string()) .await; @@ -134,35 +135,35 @@ async fn handle_socket_message(msg: ClientMessage) -> Result { } } - if msg.has_meta() { - let file = &msg.meta().file; + if req.has_meta() { + let file = &req.meta().file; let mut msg = metadata(lib, file).await; - msg.id = msg.id; + msg.nonce = req.nonce; let bytes = msg.write_to_bytes().unwrap(); let packet = ws::Message::Binary(bytes); return Ok(packet); } - if msg.has_image() { - let file = &msg.image().file; // should be the hash, + if req.has_image() { + let file = &req.image().file; // should be the hash, let image = cached_thumb(file).await; // then this doesnt need to look for the hash itself let mut img_msg = schema::ImageMessage::new(); img_msg.image = image; let mut msg = schema::Message::new(); - msg.id = msg.id; + msg.nonce = req.nonce; msg.set_image(img_msg); let bytes = msg.write_to_bytes().unwrap(); let packet = ws::Message::Binary(bytes); return Ok(packet); } - if msg.has_postmeta() { - let file = &msg.postmeta().file; - let rating = msg.postmeta().rating.unwrap(); + if req.has_postmeta() { + let file = &req.postmeta().file; + let rating = req.postmeta().rating.unwrap(); lib.set_rating(file.clone(), rating).await?; let mut msg = schema::Message::new(); - msg.id = msg.id; + msg.nonce = req.nonce; msg.set_index(get_index_msg(lib, ["default".to_string()].to_vec()).await); let bytes = msg.write_to_bytes().unwrap(); let packet = ws::Message::Binary(bytes); diff --git a/packages/proto/src/protos/schema.proto b/packages/proto/src/protos/schema.proto index 76833c7..d3b0658 100644 --- a/packages/proto/src/protos/schema.proto +++ b/packages/proto/src/protos/schema.proto @@ -62,7 +62,7 @@ message SystemInfo { } message Message { - optional int32 id = 1; + optional string nonce = 1; optional string message = 2; optional bool error = 3; oneof msg { @@ -100,7 +100,7 @@ message PostFileMetadata { } message ClientMessage { - optional int32 id = 1; + optional string nonce = 1; oneof msg { CreateLibraryMessage create = 5; RequestLibraryIndex index = 6; diff --git a/packages/ui/src/components/Explorer.tsx b/packages/ui/src/components/Explorer.tsx index 8546318..4c24945 100644 --- a/packages/ui/src/components/Explorer.tsx +++ b/packages/ui/src/components/Explorer.tsx @@ -18,18 +18,17 @@ export default function ExplorerView(props: { const metadataAccessor = useAccessor(createMetadataAccessor); const locationsAccessor = useAccessor(createLocationsAccessor); - locationsAccessor.params({ - query: {}, - }); + locationsAccessor.query({}); const [selectedLocations, setSelectedLocations] = createSignal([]); createEffect(() => { - indexAccessor.params({ - query: { - locations: selectedLocations(), - }, + indexAccessor.query({ + locations: selectedLocations(), }); + }); + + createEffect(() => { const locs = locationsAccessor.data() || []; if (selectedLocations().length === 0 && locs.length > 0) { setSelectedLocations([locs[0].id]); @@ -270,13 +269,11 @@ export default function ExplorerView(props: { return ( { - const ids = metadataAccessor.params()?.query?.ids || []; + const ids = metadataAccessor.query()?.ids || []; const id = items[0].path; if (!ids.includes(id)) { - metadataAccessor.params({ - query: { - ids: [...ids, items[0].path], - }, + metadataAccessor.query({ + ids: [...ids, items[0].path], }); } }} diff --git a/packages/ui/src/components/Viewer.tsx b/packages/ui/src/components/Viewer.tsx index dd5ccc1..60d648b 100644 --- a/packages/ui/src/components/Viewer.tsx +++ b/packages/ui/src/components/Viewer.tsx @@ -1,14 +1,12 @@ import { ParentProps, createEffect, createSignal, on, onMount } from 'solid-js'; import { DynamicImage } from 'tokyo-api/src/DynamicImage.ts'; -// import storage from '../services/ClientStorage.worker'; -// import * as viewport from 'tokyo-viewport'; import Button from './Button.tsx'; import Icon from './Icon.tsx'; import { Stars } from './Stars.tsx'; -import { Notifications } from './notifications/Notifications.ts'; import { t } from 'tokyo-locales'; -import { createMetadataAccessor } from 'tokyo-api'; +import { createImageAccessor, createMetadataAccessor } from 'tokyo-api'; import { getImage } from 'tauri-plugin-tokyo'; +import { useAccessor } from 'tokyo-accessors/src/adapters/solid.ts'; const [app, setApp] = createSignal({ zoom: 1, @@ -37,6 +35,8 @@ viewportCanvas.style.position = 'absolute'; export default function Preview(props: { file: any; onClose?: () => void }) { const [loading, setLoading] = createSignal(true); + const [settings] = createSignal({}); + let timeout: number; const resize = () => { @@ -45,33 +45,23 @@ export default function Preview(props: { file: any; onClose?: () => void }) { viewportCanvas.height = parent?.clientHeight * 2; }; - let vp: viewport.WebHandle; - - const metadata = createMetadataAccessor(); + const metadata = useAccessor(createMetadataAccessor); + const image = useAccessor(createImageAccessor); createEffect(async () => { const _item = props.file; clearTimeout(timeout); - metadata.setParams({ - query: { - ids: [_item.path], - }, - }); - - createEffect(() => { - const edit = settings(); - if (vp) { - vp.apply_edit(edit); - } + metadata.query({ + ids: [_item.path], }); createEffect( on( - () => [...metadata.store, settings()], + () => [...(metadata.data() || []), settings()], () => { - const meta = metadata.store[0]; + const meta = metadata.data()?.[0]; if (meta) { console.log('Load', _item.path, settings()); @@ -113,18 +103,6 @@ export default function Preview(props: { file: any; onClose?: () => void }) { ); window.addEventListener('resize', resize); - - viewport - .default() - .then(async () => { - const handle = await viewport.init(); - vp = handle; - return handle; - }) - .catch((err) => { - console.error('Viewport Error: ', err); - Notifications.error(err.message); - }); }); onMount(() => {