perf: move PouchDB/IndexedDB operations to a Web Worker
All database reads and writes now run off the main thread via a dedicated worker, eliminating IndexedDB overhead from the frame budget. - Create data.worker.ts exposing the Data class via Comlink - Refactor data.ts to export a Comlink-wrapped proxy and a standalone generateId() pure function (workers can't expose sync methods cleanly) - Update all 10 call sites to import generateId directly instead of calling data.generateId() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@
|
|||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.6.0",
|
||||||
"@vueuse/router": "^13.6.0",
|
"@vueuse/router": "^13.6.0",
|
||||||
"arktype": "^2.1.29",
|
"arktype": "^2.1.29",
|
||||||
|
"comlink": "^4.4.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"font-color-contrast": "^11.1.0",
|
"font-color-contrast": "^11.1.0",
|
||||||
|
|||||||
66
pnpm-lock.yaml
generated
66
pnpm-lock.yaml
generated
@@ -53,6 +53,9 @@ importers:
|
|||||||
arktype:
|
arktype:
|
||||||
specifier: ^2.1.29
|
specifier: ^2.1.29
|
||||||
version: 2.1.29
|
version: 2.1.29
|
||||||
|
comlink:
|
||||||
|
specifier: ^4.4.2
|
||||||
|
version: 4.4.2
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -1053,12 +1056,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.4.0':
|
|
||||||
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
|
||||||
peerDependencies:
|
|
||||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.7.0':
|
'@eslint-community/eslint-utils@4.7.0':
|
||||||
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@@ -1069,10 +1066,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.6.2':
|
|
||||||
resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==}
|
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
|
||||||
|
|
||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
|
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@@ -2985,6 +2978,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
comlink@4.4.2:
|
||||||
|
resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==}
|
||||||
|
|
||||||
commander@2.20.3:
|
commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
@@ -3275,15 +3271,6 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
debug@4.3.4:
|
|
||||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
|
||||||
engines: {node: '>=6.0'}
|
|
||||||
peerDependencies:
|
|
||||||
supports-color: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
supports-color:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -4959,9 +4946,6 @@ packages:
|
|||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
ms@2.1.2:
|
|
||||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -7642,25 +7626,17 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.25.5':
|
'@esbuild/win32-x64@0.25.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)':
|
|
||||||
dependencies:
|
|
||||||
eslint: 8.57.1
|
|
||||||
eslint-visitor-keys: 3.4.3
|
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
|
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.1':
|
'@eslint-community/regexpp@4.12.1': {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.6.2': {}
|
|
||||||
|
|
||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.3.4
|
debug: 4.4.3
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.20.0
|
globals: 13.20.0
|
||||||
ignore: 5.2.4
|
ignore: 5.2.4
|
||||||
@@ -7676,7 +7652,7 @@ snapshots:
|
|||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
debug: 4.3.4
|
debug: 4.4.3
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -8656,7 +8632,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.46.2
|
'@typescript-eslint/visitor-keys': 8.46.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -8676,7 +8652,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -8707,7 +8683,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -8742,7 +8718,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/visitor-keys': 8.46.2
|
'@typescript-eslint/visitor-keys': 8.46.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
@@ -9542,6 +9518,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
|
comlink@4.4.2: {}
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@7.2.0: {}
|
commander@7.2.0: {}
|
||||||
@@ -9836,10 +9814,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.3.4:
|
|
||||||
dependencies:
|
|
||||||
ms: 2.1.2
|
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -10155,8 +10129,8 @@ snapshots:
|
|||||||
|
|
||||||
eslint@8.57.1:
|
eslint@8.57.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
|
||||||
'@eslint-community/regexpp': 4.6.2
|
'@eslint-community/regexpp': 4.12.1
|
||||||
'@eslint/eslintrc': 2.1.4
|
'@eslint/eslintrc': 2.1.4
|
||||||
'@eslint/js': 8.57.1
|
'@eslint/js': 8.57.1
|
||||||
'@humanwhocodes/config-array': 0.13.0
|
'@humanwhocodes/config-array': 0.13.0
|
||||||
@@ -10165,8 +10139,8 @@ snapshots:
|
|||||||
'@ungap/structured-clone': 1.2.0
|
'@ungap/structured-clone': 1.2.0
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.6
|
||||||
debug: 4.3.4
|
debug: 4.4.3
|
||||||
doctrine: 3.0.0
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 7.2.2
|
eslint-scope: 7.2.2
|
||||||
@@ -11745,8 +11719,6 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.0.0: {}
|
ms@2.0.0: {}
|
||||||
|
|
||||||
ms@2.1.2: {}
|
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
multiformats@9.9.0: {}
|
multiformats@9.9.0: {}
|
||||||
|
|||||||
169
src/data/data.ts
169
src/data/data.ts
@@ -1,166 +1,31 @@
|
|||||||
|
import { wrap } from "comlink"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
import indexedDb from "pouchdb-adapter-indexeddb"
|
|
||||||
import PouchDb from "pouchdb-browser"
|
|
||||||
|
|
||||||
import { DataType } from "./DataType.enum"
|
import { DataType } from "./DataType.enum"
|
||||||
import { Model } from "./models/Model"
|
import { Model } from "./models/Model"
|
||||||
|
|
||||||
PouchDb.plugin(indexedDb)
|
export interface DataApi {
|
||||||
|
add<DT extends DataType>(model: Model<DT>): Promise<boolean>
|
||||||
interface GetAllParams {
|
update<DT extends DataType, T extends Model<DT>>(model: T): Promise<boolean>
|
||||||
|
remove(id: string): Promise<boolean>
|
||||||
|
get<DT extends DataType, T extends Model<DT>>(id: string): Promise<T | null>
|
||||||
|
getOrCreate<DT extends DataType, T extends Model<DT>>(
|
||||||
|
id: string,
|
||||||
|
initialValue: T
|
||||||
|
): Promise<T>
|
||||||
|
getAll<DT extends DataType, T extends Model<DT>>(params: {
|
||||||
prefix?: string
|
prefix?: string
|
||||||
includeDocs?: boolean
|
includeDocs?: boolean
|
||||||
includeAttachments?: boolean
|
includeAttachments?: boolean
|
||||||
keys?: string[]
|
keys?: string[]
|
||||||
|
}): Promise<T[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
class Data {
|
export const generateId = (type?: DataType | string, id?: string): string => {
|
||||||
// oxlint-disable-next-line typescript/ban-types
|
if (!type) return id || nanoid()
|
||||||
private readonly locale: PouchDB.Database<{}> | null = null
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
try {
|
|
||||||
this.locale = new PouchDb("remanso", {
|
|
||||||
adapter: "indexeddb"
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("data error", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async add<DT extends DataType>(model: Model<DT>): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const result = await this.locale?.put(model)
|
|
||||||
return result?.ok ?? false
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update<DT extends DataType, T extends Model<DT>>(
|
|
||||||
model: T
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
if (!model._id) {
|
|
||||||
const result = await this.locale?.put(model)
|
|
||||||
return result?.ok ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldModel = await this.get(model._id)
|
|
||||||
if (oldModel) {
|
|
||||||
const result = await this.locale?.put({ ...oldModel, ...model })
|
|
||||||
return result?.ok ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.locale?.put(model)
|
|
||||||
return result?.ok ?? false
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(id: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const doc = await this.get(id)
|
|
||||||
if (!doc) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const result = await this.locale?.put({
|
|
||||||
...doc,
|
|
||||||
_deleted: true
|
|
||||||
})
|
|
||||||
return result?.ok ?? false
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get<DT extends DataType, T extends Model<DT>>(
|
|
||||||
id: string
|
|
||||||
): Promise<T | null> {
|
|
||||||
try {
|
|
||||||
return ((await this.locale?.get(id)) as T) || null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getOrCreate<DT extends DataType, T extends Model<DT>>(
|
|
||||||
id: string,
|
|
||||||
initialValue: T
|
|
||||||
): Promise<T> {
|
|
||||||
const element = await this.get<DT, T>(id)
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
return element
|
|
||||||
}
|
|
||||||
|
|
||||||
await data.add<DT>({ ...initialValue, _id: id })
|
|
||||||
|
|
||||||
return this.getOrCreate(id, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAll<DT extends DataType, T extends Model<DT>>({
|
|
||||||
prefix,
|
|
||||||
includeDocs = true,
|
|
||||||
includeAttachments = false,
|
|
||||||
keys = []
|
|
||||||
}: GetAllParams): Promise<T[]> {
|
|
||||||
if (!this.locale) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keys.length) {
|
|
||||||
const response = await this.locale.allDocs({
|
|
||||||
include_docs: includeDocs,
|
|
||||||
attachments: includeAttachments,
|
|
||||||
keys: keys.map((key) => this.generateId(prefix, key))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (includeDocs) {
|
|
||||||
return response.rows
|
|
||||||
.map((row) => {
|
|
||||||
if ("error" in row) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return row.doc
|
|
||||||
})
|
|
||||||
.filter(Boolean) as T[]
|
|
||||||
} else {
|
|
||||||
return response.rows
|
|
||||||
.map((row) => {
|
|
||||||
if ("error" in row) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return { _id: row.id }
|
|
||||||
})
|
|
||||||
.filter(Boolean) as T[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.locale.allDocs({
|
|
||||||
include_docs: includeDocs,
|
|
||||||
attachments: includeAttachments,
|
|
||||||
startkey: prefix ? prefix : undefined,
|
|
||||||
endkey: prefix ? `${prefix}\ufff0` : undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.rows.map((row) => row.doc) as T[]
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateId(type?: DataType | string, id?: string) {
|
|
||||||
if (!type) {
|
|
||||||
return id || nanoid()
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${type}-${id || nanoid()}`
|
return `${type}-${id || nanoid()}`
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const data = new Data()
|
import DataWorker from "./data.worker?worker"
|
||||||
|
|
||||||
|
export const data = wrap(new DataWorker()) as unknown as DataApi
|
||||||
|
|||||||
156
src/data/data.worker.ts
Normal file
156
src/data/data.worker.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { expose } from "comlink"
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
import indexedDb from "pouchdb-adapter-indexeddb"
|
||||||
|
import PouchDb from "pouchdb-browser"
|
||||||
|
|
||||||
|
import { DataType } from "./DataType.enum"
|
||||||
|
import { Model } from "./models/Model"
|
||||||
|
|
||||||
|
PouchDb.plugin(indexedDb)
|
||||||
|
|
||||||
|
interface GetAllParams {
|
||||||
|
prefix?: string
|
||||||
|
includeDocs?: boolean
|
||||||
|
includeAttachments?: boolean
|
||||||
|
keys?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Data {
|
||||||
|
// oxlint-disable-next-line typescript/ban-types
|
||||||
|
private readonly locale: PouchDB.Database<{}> | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
try {
|
||||||
|
this.locale = new PouchDb("remanso", {
|
||||||
|
adapter: "indexeddb"
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("data error", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildId(type?: DataType | string, id?: string): string {
|
||||||
|
if (!type) return id || nanoid()
|
||||||
|
return `${type}-${id || nanoid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public async add<DT extends DataType>(model: Model<DT>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.locale?.put(model)
|
||||||
|
return result?.ok ?? false
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update<DT extends DataType, T extends Model<DT>>(
|
||||||
|
model: T
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!model._id) {
|
||||||
|
const result = await this.locale?.put(model)
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldModel = await this.get(model._id)
|
||||||
|
if (oldModel) {
|
||||||
|
const result = await this.locale?.put({ ...oldModel, ...model })
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.locale?.put(model)
|
||||||
|
return result?.ok ?? false
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const doc = await this.get(id)
|
||||||
|
if (!doc) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const result = await this.locale?.put({
|
||||||
|
...doc,
|
||||||
|
_deleted: true
|
||||||
|
})
|
||||||
|
return result?.ok ?? false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get<DT extends DataType, T extends Model<DT>>(
|
||||||
|
id: string
|
||||||
|
): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
return ((await this.locale?.get(id)) as T) || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getOrCreate<DT extends DataType, T extends Model<DT>>(
|
||||||
|
id: string,
|
||||||
|
initialValue: T
|
||||||
|
): Promise<T> {
|
||||||
|
const element = await this.get<DT, T>(id)
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.add<DT>({ ...initialValue, _id: id })
|
||||||
|
|
||||||
|
return this.getOrCreate(id, initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAll<DT extends DataType, T extends Model<DT>>({
|
||||||
|
prefix,
|
||||||
|
includeDocs = true,
|
||||||
|
includeAttachments = false,
|
||||||
|
keys = []
|
||||||
|
}: GetAllParams): Promise<T[]> {
|
||||||
|
if (!this.locale) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
const response = await this.locale.allDocs({
|
||||||
|
include_docs: includeDocs,
|
||||||
|
attachments: includeAttachments,
|
||||||
|
keys: keys.map((key) => this.buildId(prefix, key))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (includeDocs) {
|
||||||
|
return response.rows
|
||||||
|
.map((row) => {
|
||||||
|
if ("error" in row) return null
|
||||||
|
return row.doc
|
||||||
|
})
|
||||||
|
.filter(Boolean) as T[]
|
||||||
|
} else {
|
||||||
|
return response.rows
|
||||||
|
.map((row) => {
|
||||||
|
if ("error" in row) return null
|
||||||
|
return { _id: row.id }
|
||||||
|
})
|
||||||
|
.filter(Boolean) as T[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.locale.allDocs({
|
||||||
|
include_docs: includeDocs,
|
||||||
|
attachments: includeAttachments,
|
||||||
|
startkey: prefix ? prefix : undefined,
|
||||||
|
endkey: prefix ? `${prefix}\ufff0` : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.rows.map((row) => row.doc) as T[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expose(new Data())
|
||||||
@@ -2,7 +2,7 @@ import { useAsyncState } from "@vueuse/core"
|
|||||||
import { ComputedRef, onUnmounted, toValue } from "vue"
|
import { ComputedRef, onUnmounted, toValue } from "vue"
|
||||||
|
|
||||||
import { backlinkEventBus } from "@/bus/backlinkEventBus"
|
import { backlinkEventBus } from "@/bus/backlinkEventBus"
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { BacklinkNote } from "@/modules/note/models/BacklinkNote"
|
import { BacklinkNote } from "@/modules/note/models/BacklinkNote"
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export const useBacklinks = (sha: string | ComputedRef<string>) => {
|
|||||||
|
|
||||||
const { state: backlink, execute } = useAsyncState(
|
const { state: backlink, execute } = useAsyncState(
|
||||||
data.get<DataType.BacklinkNote, BacklinkNote>(
|
data.get<DataType.BacklinkNote, BacklinkNote>(
|
||||||
data.generateId(DataType.BacklinkNote, sha)
|
generateId(DataType.BacklinkNote, sha)
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { watch } from "vue"
|
import { watch } from "vue"
|
||||||
|
|
||||||
import { backlinkEventBus } from "@/bus/backlinkEventBus"
|
import { backlinkEventBus } from "@/bus/backlinkEventBus"
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { useFile } from "@/hooks/useFile.hook"
|
import { useFile } from "@/hooks/useFile.hook"
|
||||||
import { Backlink } from "@/modules/note/models/Backlink"
|
import { Backlink } from "@/modules/note/models/Backlink"
|
||||||
@@ -42,7 +42,7 @@ export const useComputeBacklinks = () => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileBacklinkId = data.generateId(DataType.BacklinkNote, file.sha)
|
const fileBacklinkId = generateId(DataType.BacklinkNote, file.sha)
|
||||||
const fileBacklink = await data.get<DataType.BacklinkNote, BacklinkNote>(
|
const fileBacklink = await data.get<DataType.BacklinkNote, BacklinkNote>(
|
||||||
fileBacklinkId
|
fileBacklinkId
|
||||||
)
|
)
|
||||||
@@ -102,7 +102,7 @@ export const useComputeBacklinks = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [sha, fileBacklinks] of backlinks) {
|
for (const [sha, fileBacklinks] of backlinks) {
|
||||||
const fileBacklinkId = data.generateId(DataType.BacklinkNote, sha)
|
const fileBacklinkId = generateId(DataType.BacklinkNote, sha)
|
||||||
const backlinkNote: BacklinkNote = {
|
const backlinkNote: BacklinkNote = {
|
||||||
_id: fileBacklinkId,
|
_id: fileBacklinkId,
|
||||||
$type: DataType.BacklinkNote,
|
$type: DataType.BacklinkNote,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useAsyncState } from "@vueuse/core"
|
import { useAsyncState } from "@vueuse/core"
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
|
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
|
||||||
import { Note } from "@/modules/note/models/Note"
|
import { Note } from "@/modules/note/models/Note"
|
||||||
@@ -36,7 +36,7 @@ export const useOfflineNotes = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!file.sha ||
|
!file.sha ||
|
||||||
cachedNotesSet.has(data.generateId(DataType.Note, file.sha))
|
cachedNotesSet.has(generateId(DataType.Note, file.sha))
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useAsyncState } from "@vueuse/core"
|
|||||||
import { addDays, isAfter } from "date-fns"
|
import { addDays, isAfter } from "date-fns"
|
||||||
import { computed, nextTick, watch } from "vue"
|
import { computed, nextTick, watch } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { useFile } from "@/hooks/useFile.hook"
|
import { useFile } from "@/hooks/useFile.hook"
|
||||||
import { useLinks } from "@/hooks/useLinks.hook"
|
import { useLinks } from "@/hooks/useLinks.hook"
|
||||||
@@ -51,7 +51,7 @@ export const useSpacedRepetitionCards = () => {
|
|||||||
const repetition = await data.getOrCreate<
|
const repetition = await data.getOrCreate<
|
||||||
DataType.RepetitionCard,
|
DataType.RepetitionCard,
|
||||||
RepetitionCard
|
RepetitionCard
|
||||||
>(data.generateId(DataType.RepetitionCard, cardFile.path), {
|
>(generateId(DataType.RepetitionCard, cardFile.path), {
|
||||||
$type: DataType.RepetitionCard,
|
$type: DataType.RepetitionCard,
|
||||||
level: 1,
|
level: 1,
|
||||||
repeatDate: new Date(),
|
repeatDate: new Date(),
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useAsyncState } from "@vueuse/core"
|
import { useAsyncState } from "@vueuse/core"
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { History } from "@/data/models/History"
|
import { History } from "@/data/models/History"
|
||||||
|
|
||||||
const HISTORY_ID = data.generateId(DataType.History, "history")
|
const HISTORY_ID = generateId(DataType.History, "history")
|
||||||
|
|
||||||
export const useLastVisitedRepos = () => {
|
export const useLastVisitedRepos = () => {
|
||||||
const history = useAsyncState(
|
const history = useAsyncState(
|
||||||
() =>
|
() =>
|
||||||
data.get<DataType.History, History>(
|
data.get<DataType.History, History>(
|
||||||
data.generateId(DataType.History, "history")
|
generateId(DataType.History, "history")
|
||||||
),
|
),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Ref, toValue } from "vue"
|
import { Ref, toValue } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { History } from "@/data/models/History"
|
import { History } from "@/data/models/History"
|
||||||
|
|
||||||
const HISTORY_ID = data.generateId(DataType.History, "history")
|
const HISTORY_ID = generateId(DataType.History, "history")
|
||||||
const MAX_REPO_HISTORY = 10
|
const MAX_REPO_HISTORY = 10
|
||||||
|
|
||||||
export const useVisitRepo = (newRepo: {
|
export const useVisitRepo = (newRepo: {
|
||||||
|
|||||||
6
src/modules/note/cache/prepareNoteCache.ts
vendored
6
src/modules/note/cache/prepareNoteCache.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { Note } from "@/modules/note/models/Note"
|
import { Note } from "@/modules/note/models/Note"
|
||||||
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||||
@@ -14,8 +14,8 @@ type NoteCacheResult =
|
|||||||
export const prepareNoteCache = (sha: string, path?: string) => {
|
export const prepareNoteCache = (sha: string, path?: string) => {
|
||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
|
|
||||||
const noteId = data.generateId(DataType.Note, sha)
|
const noteId = generateId(DataType.Note, sha)
|
||||||
const notePath = path ? data.generateId(DataType.Note, path) : null
|
const notePath = path ? generateId(DataType.Note, path) : null
|
||||||
const getCachedNote = async (): Promise<NoteCacheResult> => {
|
const getCachedNote = async (): Promise<NoteCacheResult> => {
|
||||||
const note = await data.get<DataType.Note, Note>(noteId)
|
const note = await data.get<DataType.Note, Note>(noteId)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { computed, onMounted, ref } from "vue"
|
import { computed, onMounted, ref } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { useRepos } from "@/hooks/useRepos.hook"
|
import { useRepos } from "@/hooks/useRepos.hook"
|
||||||
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
|
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
|
||||||
@@ -27,7 +27,7 @@ export const useFavoriteRepos = () => {
|
|||||||
|
|
||||||
const toggleFavorite = async (repo: RepoBase, isFavorite: boolean) => {
|
const toggleFavorite = async (repo: RepoBase, isFavorite: boolean) => {
|
||||||
const favorite: FavoriteRepo = {
|
const favorite: FavoriteRepo = {
|
||||||
_id: data.generateId(DataType.FavoriteRepo, repo.id),
|
_id: generateId(DataType.FavoriteRepo, repo.id),
|
||||||
$type: DataType.FavoriteRepo,
|
$type: DataType.FavoriteRepo,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
name: repo.name,
|
name: repo.name,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
|
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
|
||||||
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
|
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
|
||||||
@@ -39,7 +39,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
this.user = user
|
this.user = user
|
||||||
this.repo = repo
|
this.repo = repo
|
||||||
|
|
||||||
const savedRepoId = data.generateId(DataType.SavedRepo, `${user}-${repo}`)
|
const savedRepoId = generateId(DataType.SavedRepo, `${user}-${repo}`)
|
||||||
const userSettingsId = `UserSetting-${user}-${repo}`
|
const userSettingsId = `UserSetting-${user}-${repo}`
|
||||||
|
|
||||||
const [cachedSavedRepo, cachedUserSettings] = await Promise.all([
|
const [cachedSavedRepo, cachedUserSettings] = await Promise.all([
|
||||||
@@ -131,7 +131,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedRepoId = data.generateId(
|
const savedRepoId = generateId(
|
||||||
DataType.SavedRepo,
|
DataType.SavedRepo,
|
||||||
`${this.user}-${this.repo}`
|
`${this.user}-${this.repo}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Octokit } from "@octokit/rest"
|
import { Octokit } from "@octokit/rest"
|
||||||
import { addMinutes, addSeconds, isBefore } from "date-fns"
|
import { addMinutes, addSeconds, isBefore } from "date-fns"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { GithubAccessToken } from "@/data/models/GithubAccessToken"
|
import { GithubAccessToken } from "@/data/models/GithubAccessToken"
|
||||||
import { GithubToken } from "@/modules/user/interfaces/GithubToken"
|
import { GithubToken } from "@/modules/user/interfaces/GithubToken"
|
||||||
@@ -26,7 +26,7 @@ export const needToRefreshToken = async () => {
|
|||||||
const accessToken = await data.get<
|
const accessToken = await data.get<
|
||||||
DataType.GithubAccessToken,
|
DataType.GithubAccessToken,
|
||||||
GithubAccessToken
|
GithubAccessToken
|
||||||
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
|
>(generateId(DataType.GithubAccessToken, personalTokenId))
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return false
|
return false
|
||||||
@@ -42,7 +42,7 @@ export const refreshToken = async () => {
|
|||||||
const accessToken = await data.get<
|
const accessToken = await data.get<
|
||||||
DataType.GithubAccessToken,
|
DataType.GithubAccessToken,
|
||||||
GithubAccessToken
|
GithubAccessToken
|
||||||
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
|
>(generateId(DataType.GithubAccessToken, personalTokenId))
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return null
|
return null
|
||||||
@@ -74,7 +74,7 @@ export const getAccessToken = async () => {
|
|||||||
const response = await data.get<
|
const response = await data.get<
|
||||||
DataType.GithubAccessToken,
|
DataType.GithubAccessToken,
|
||||||
GithubAccessToken
|
GithubAccessToken
|
||||||
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
|
>(generateId(DataType.GithubAccessToken, personalTokenId))
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ export const saveAccessToken = async (githubToken: GithubToken) => {
|
|||||||
|
|
||||||
const accessToken: GithubAccessToken = {
|
const accessToken: GithubAccessToken = {
|
||||||
...actualPAT,
|
...actualPAT,
|
||||||
_id: data.generateId(DataType.GithubAccessToken, personalTokenId),
|
_id: generateId(DataType.GithubAccessToken, personalTokenId),
|
||||||
$type: DataType.GithubAccessToken,
|
$type: DataType.GithubAccessToken,
|
||||||
token: githubToken.access_token,
|
token: githubToken.access_token,
|
||||||
expiresIn: githubToken.expires_in,
|
expiresIn: githubToken.expires_in,
|
||||||
|
|||||||
Reference in New Issue
Block a user