chore: restructure vue-sync-engine workspace and remove unused files
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { shallowRef, triggerRef, type ShallowRef } from 'vue'
|
||||
import type { EntityId, EntityPatch, Patch, QueryStatus } from '../core/types'
|
||||
import { Op, Status } from '../core/flags'
|
||||
import { applyPatch } from '../core/patches'
|
||||
|
||||
export interface QueryState<T = unknown> {
|
||||
status: QueryStatus
|
||||
data: T | undefined
|
||||
error: { message: string } | undefined
|
||||
}
|
||||
|
||||
export function createMirror() {
|
||||
const entities = new Map<string, Map<EntityId, unknown>>()
|
||||
const versions = new Map<string, ShallowRef<number>>()
|
||||
const queries = new Map<string, ShallowRef<QueryState>>()
|
||||
|
||||
function typeVersion(type: string): ShallowRef<number> {
|
||||
let v = versions.get(type)
|
||||
if (!v) {
|
||||
v = shallowRef(0)
|
||||
versions.set(type, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
function entityBucket(type: string): Map<EntityId, unknown> {
|
||||
let b = entities.get(type)
|
||||
if (!b) {
|
||||
b = new Map()
|
||||
entities.set(type, b)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
function getEntity<T>(type: string, id: EntityId): T | undefined {
|
||||
typeVersion(type).value
|
||||
const b = entities.get(type)
|
||||
return b === undefined ? undefined : (b.get(id) as T | undefined)
|
||||
}
|
||||
|
||||
function applyEntityPatches(patches: EntityPatch[]): void {
|
||||
if (patches.length === 0) return
|
||||
let lastType = ''
|
||||
let bucket: Map<EntityId, unknown> | undefined
|
||||
let touchedFirst: string | undefined
|
||||
let touchedRest: Set<string> | undefined
|
||||
for (let i = 0; i < patches.length; i++) {
|
||||
const p = patches[i]
|
||||
if (p.type !== lastType) {
|
||||
lastType = p.type
|
||||
bucket = entityBucket(lastType)
|
||||
if (touchedFirst === undefined) touchedFirst = lastType
|
||||
else if (lastType !== touchedFirst) {
|
||||
if (touchedRest === undefined) touchedRest = new Set()
|
||||
touchedRest.add(lastType)
|
||||
}
|
||||
}
|
||||
const patch = p.patch
|
||||
if (patch.op === Op.Delete && patch.path.length === 0) {
|
||||
bucket!.delete(p.id)
|
||||
} else {
|
||||
bucket!.set(p.id, applyPatch(bucket!.get(p.id), patch))
|
||||
}
|
||||
}
|
||||
if (touchedFirst !== undefined) triggerRef(typeVersion(touchedFirst))
|
||||
if (touchedRest !== undefined) for (const t of touchedRest) triggerRef(typeVersion(t))
|
||||
}
|
||||
|
||||
function ensureQuery<T>(subId: string): ShallowRef<QueryState<T>> {
|
||||
let r = queries.get(subId) as ShallowRef<QueryState<T>> | undefined
|
||||
if (!r) {
|
||||
r = shallowRef<QueryState<T>>({ status: Status.Idle, data: undefined, error: undefined })
|
||||
queries.set(subId, r as ShallowRef<QueryState>)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
function applyQueryPatch(subId: string, status: QueryStatus, patch?: Patch, error?: { message: string }): void {
|
||||
const r = ensureQuery(subId)
|
||||
const prev = r.value
|
||||
r.value = {
|
||||
status,
|
||||
data: patch ? applyPatch(prev.data, patch) : prev.data,
|
||||
error: error ?? prev.error,
|
||||
}
|
||||
}
|
||||
|
||||
function dropQuery(subId: string): void {
|
||||
queries.delete(subId)
|
||||
}
|
||||
|
||||
return { entities, getEntity, applyEntityPatches, ensureQuery, applyQueryPatch, dropQuery }
|
||||
}
|
||||
|
||||
export type Mirror = ReturnType<typeof createMirror>
|
||||
@@ -0,0 +1,113 @@
|
||||
import { effectScope, type EffectScope } from 'vue'
|
||||
import type { Transport } from '../transport/protocol'
|
||||
import type { Mirror } from './mirror'
|
||||
import { hashKey } from '../core/queryKey'
|
||||
import { Msg } from '../core/flags'
|
||||
|
||||
interface QuerySubHandle {
|
||||
subId: string
|
||||
refCount: number
|
||||
scope: EffectScope
|
||||
gcTimer: ReturnType<typeof setTimeout> | null
|
||||
release: () => void
|
||||
fetchNextPage: () => void
|
||||
}
|
||||
|
||||
export interface TabRuntime {
|
||||
mirror: Mirror
|
||||
transport: Transport
|
||||
subscribeQuery(defName: string, key: readonly unknown[], args: unknown): QuerySubHandle
|
||||
mutate(defName: string, input: unknown): Promise<unknown>
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
export interface TabRuntimeOptions {
|
||||
transport: Transport
|
||||
mirror: Mirror
|
||||
staleSubGcMs?: number
|
||||
}
|
||||
|
||||
export function createTabRuntime(opts: TabRuntimeOptions): TabRuntime {
|
||||
const { transport, mirror } = opts
|
||||
const staleSubGcMs = opts.staleSubGcMs ?? 5_000
|
||||
|
||||
const byKey = new Map<string, QuerySubHandle>()
|
||||
const pendingMutations = new Map<string, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>()
|
||||
const tabId =
|
||||
(typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID()
|
||||
: Math.random().toString(36).slice(2)) + '-'
|
||||
let subSeq = 0
|
||||
let mutSeq = 0
|
||||
|
||||
const off = transport.onMessage((msg) => {
|
||||
if (msg.type === Msg.QueryPatch) {
|
||||
mirror.applyQueryPatch(msg.subId, msg.status, msg.patch, msg.error)
|
||||
} else if (msg.type === Msg.EntityPatch) {
|
||||
mirror.applyEntityPatches(msg.patches)
|
||||
} else if (msg.type === Msg.MutateResult) {
|
||||
const p = pendingMutations.get(msg.mutId)
|
||||
if (p) {
|
||||
pendingMutations.delete(msg.mutId)
|
||||
if (msg.ok) p.resolve(msg.data)
|
||||
else p.reject(new Error(msg.error?.message ?? 'mutation failed'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function subscribeQuery(defName: string, key: readonly unknown[], args: unknown): QuerySubHandle {
|
||||
const hash = hashKey(key)
|
||||
const existing = byKey.get(hash)
|
||||
if (existing) {
|
||||
if (existing.gcTimer !== null) {
|
||||
clearTimeout(existing.gcTimer)
|
||||
existing.gcTimer = null
|
||||
}
|
||||
existing.refCount++
|
||||
return existing
|
||||
}
|
||||
|
||||
const subId = `${tabId}s${++subSeq}`
|
||||
const scope = effectScope(true)
|
||||
mirror.ensureQuery(subId)
|
||||
transport.send({ type: Msg.Subscribe, subId, defName, args })
|
||||
|
||||
const handle: QuerySubHandle = {
|
||||
subId,
|
||||
refCount: 1,
|
||||
scope,
|
||||
gcTimer: null,
|
||||
fetchNextPage() {
|
||||
transport.send({ type: Msg.FetchNextPage, subId })
|
||||
},
|
||||
release() {
|
||||
handle.refCount--
|
||||
if (handle.refCount > 0) return
|
||||
handle.gcTimer = setTimeout(() => {
|
||||
byKey.delete(hash)
|
||||
transport.send({ type: Msg.Unsubscribe, subId })
|
||||
mirror.dropQuery(subId)
|
||||
scope.stop()
|
||||
}, staleSubGcMs)
|
||||
},
|
||||
}
|
||||
byKey.set(hash, handle)
|
||||
return handle
|
||||
}
|
||||
|
||||
function mutate(defName: string, input: unknown): Promise<unknown> {
|
||||
const mutId = `${tabId}m${++mutSeq}`
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingMutations.set(mutId, { resolve, reject })
|
||||
transport.send({ type: Msg.Mutate, mutId, defName, input })
|
||||
})
|
||||
}
|
||||
|
||||
function dispose(): void {
|
||||
off()
|
||||
for (const h of byKey.values()) h.scope.stop()
|
||||
byKey.clear()
|
||||
}
|
||||
|
||||
return { mirror, transport, subscribeQuery, mutate, dispose }
|
||||
}
|
||||
Reference in New Issue
Block a user