[WEB-2706] chore: Switch to wa-sqlite (#5859)
* fix layout switching when filter is not yet completely fetched * add layout in issue filter params * Handle cases when DB intilization failed * chore: permission layer and updated issues v1 query from workspace to project level * - Switch to using wa-sqlite instead of sqlite-wasm * Code cleanup and fix indexes * Add missing files * - Import only required functions from sentry - Wait till all the tables are created * Skip workspace sync if one is already in progress. * Sync workspace without using transaction * Minor cleanup * Close DB connection before deleting files Fix clear OPFS on safari * Fix type issue * Improve issue insert performance * Refactor workspace sync * Close the DB connection while switching workspaces * Update web/core/local-db/worker/db.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Worker cleanup and error handling Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update web/core/local-db/worker/db.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update web/core/local-db/storage.sqlite.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update web/core/local-db/worker/db.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Code cleanup * Set default order by to created at and descending * Wait for transactions to complete. --------- Co-authored-by: rahulramesha <rahulramesham@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
ad25a972a1
commit
9fb353ef54
38 changed files with 4318 additions and 220 deletions
3
packages/types/src/view-props.d.ts
vendored
3
packages/types/src/view-props.d.ts
vendored
|
|
@ -77,7 +77,8 @@ export type TIssueParams =
|
|||
| "show_empty_groups"
|
||||
| "cursor"
|
||||
| "per_page"
|
||||
| "issue_type";
|
||||
| "issue_type"
|
||||
| "layout";
|
||||
|
||||
export type TCalendarLayouts = "month" | "week";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useParams } from "next/navigation";
|
|||
import useSWR from "swr";
|
||||
// mobx store
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, IssuePeekOverview } from "@/components/issues";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// ui
|
||||
|
|
@ -16,7 +17,7 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
|||
// hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
|
||||
useSWR(
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
|
|
@ -26,7 +27,17 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.ARCHIVED}>
|
||||
<ArchivedIssueAppliedFiltersRoot />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useParams } from "next/navigation";
|
|||
import useSWR from "swr";
|
||||
// hooks
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { TransferIssues, TransferIssuesModal } from "@/components/cycles";
|
||||
import {
|
||||
CycleAppliedFiltersRoot,
|
||||
|
|
@ -50,7 +51,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||
// state
|
||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
|
||||
useSWR(
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId && cycleId
|
||||
? `CYCLE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${cycleId.toString()}`
|
||||
: null,
|
||||
|
|
@ -62,7 +63,8 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
const issueFilters = issuesFilter?.getIssueFilters(cycleId?.toString());
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
|
||||
|
|
@ -75,6 +77,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||
|
||||
if (!workspaceSlug || !projectId || !cycleId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.CYCLE}>
|
||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
// hooks
|
||||
|
|
@ -30,7 +31,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
|||
// hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
|
||||
|
||||
useSWR(
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `DRAFT_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
|
|
@ -40,10 +41,18 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.DRAFT}>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import useSWR from "swr";
|
|||
// mobx store
|
||||
// components
|
||||
import { Row, ERowVariant } from "@plane/ui";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import {
|
||||
IssuePeekOverview,
|
||||
ModuleAppliedFiltersRoot,
|
||||
|
|
@ -43,7 +44,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
|||
// hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.MODULE);
|
||||
|
||||
useSWR(
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId && moduleId
|
||||
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
|
||||
: null,
|
||||
|
|
@ -55,9 +56,18 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const issueFilters = issuesFilter?.getIssueFilters(moduleId?.toString());
|
||||
|
||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
if (isLoading && !issueFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.MODULE}>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
|
|||
import useSWR from "swr";
|
||||
// components
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import {
|
||||
ListLayout,
|
||||
CalendarLayout,
|
||||
|
|
@ -44,7 +45,7 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||
// hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||
|
||||
useSWR(
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
|
|
@ -54,10 +55,18 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT}>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -53,11 +53,12 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
const issueFilters = issuesFilter?.getIssueFilters(viewId?.toString());
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && !issueFilters) {
|
||||
return (
|
||||
<div className="relative flex h-screen w-full items-center justify-center">
|
||||
<LogoSpinner />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
import { getActiveSpan, startSpan } from "@sentry/nextjs";
|
||||
import * as Comlink from "comlink";
|
||||
import set from "lodash/set";
|
||||
// plane
|
||||
import { EIssueGroupBYServerToProperty } from "@plane/constants";
|
||||
|
|
@ -16,15 +17,11 @@ import { loadWorkSpaceData } from "./utils/load-workspace";
|
|||
import { issueFilterCountQueryConstructor, issueFilterQueryConstructor } from "./utils/query-constructor";
|
||||
import { runQuery } from "./utils/query-executor";
|
||||
import { createTables } from "./utils/tables";
|
||||
import { getGroupedIssueResults, getSubGroupedIssueResults, log, logError, logInfo } from "./utils/utils";
|
||||
|
||||
declare module "@sqlite.org/sqlite-wasm" {
|
||||
export function sqlite3Worker1Promiser(...args: any): any;
|
||||
}
|
||||
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
|
||||
|
||||
const DB_VERSION = 1;
|
||||
const PAGE_SIZE = 1000;
|
||||
const BATCH_SIZE = 200;
|
||||
const PAGE_SIZE = 500;
|
||||
const BATCH_SIZE = 500;
|
||||
|
||||
type TProjectStatus = {
|
||||
issues: { status: undefined | "loading" | "ready" | "error" | "syncing"; sync: Promise<void> | undefined };
|
||||
|
|
@ -43,6 +40,9 @@ export class Storage {
|
|||
}
|
||||
|
||||
reset = () => {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
}
|
||||
this.db = null;
|
||||
this.status = undefined;
|
||||
this.projectStatus = {};
|
||||
|
|
@ -51,10 +51,8 @@ export class Storage {
|
|||
|
||||
clearStorage = async () => {
|
||||
try {
|
||||
const storageManager = window.navigator.storage;
|
||||
const fileSystemDirectoryHandle = await storageManager.getDirectory();
|
||||
//@ts-expect-error , clear local issue cache
|
||||
await fileSystemDirectoryHandle.remove({ recursive: true });
|
||||
await this.db.close();
|
||||
await clearOPFS();
|
||||
this.reset();
|
||||
} catch (e) {
|
||||
console.error("Error clearing sqlite sync storage", e);
|
||||
|
|
@ -62,13 +60,13 @@ export class Storage {
|
|||
};
|
||||
|
||||
initialize = async (workspaceSlug: string): Promise<boolean> => {
|
||||
if (document.hidden || !rootStore.user.localDBEnabled) return false; // return if the window gets hidden
|
||||
if (!rootStore.user.localDBEnabled) return false; // return if the window gets hidden
|
||||
|
||||
if (workspaceSlug !== this.workspaceSlug) {
|
||||
this.reset();
|
||||
}
|
||||
try {
|
||||
await Sentry.startSpan({ name: "INIT_DB" }, async () => await this._initialize(workspaceSlug));
|
||||
await startSpan({ name: "INIT_DB" }, async () => await this._initialize(workspaceSlug));
|
||||
return true;
|
||||
} catch (err) {
|
||||
logError(err);
|
||||
|
|
@ -91,71 +89,61 @@ export class Storage {
|
|||
return false;
|
||||
}
|
||||
|
||||
logInfo("Loading and initializing SQLite3 module...");
|
||||
try {
|
||||
const { DBClass } = await import("./worker/db");
|
||||
const worker = new Worker(new URL("./worker/db.ts", import.meta.url));
|
||||
const MyWorker = Comlink.wrap<typeof DBClass>(worker);
|
||||
|
||||
// Add cleanup on window unload
|
||||
window.addEventListener("unload", () => worker.terminate());
|
||||
|
||||
this.workspaceSlug = workspaceSlug;
|
||||
this.dbName = workspaceSlug;
|
||||
const { sqlite3Worker1Promiser } = await import("@sqlite.org/sqlite-wasm");
|
||||
const instance = await new MyWorker();
|
||||
await instance.init(workspaceSlug);
|
||||
|
||||
try {
|
||||
const promiser: any = await new Promise((resolve) => {
|
||||
const _promiser = sqlite3Worker1Promiser({
|
||||
onready: () => resolve(_promiser),
|
||||
});
|
||||
});
|
||||
|
||||
const configResponse = await promiser("config-get", {});
|
||||
log("Running SQLite3 version", configResponse.result.version.libVersion);
|
||||
|
||||
const openResponse = await promiser("open", {
|
||||
filename: `file:${this.dbName}.sqlite3?vfs=opfs`,
|
||||
});
|
||||
const { dbId } = openResponse;
|
||||
this.db = {
|
||||
dbId,
|
||||
exec: async (val: any) => {
|
||||
if (typeof val === "string") {
|
||||
val = { sql: val };
|
||||
}
|
||||
return promiser("exec", { dbId, ...val });
|
||||
},
|
||||
exec: instance.exec,
|
||||
close: instance.close,
|
||||
};
|
||||
|
||||
// dump DB of db version is matching
|
||||
const dbVersion = await this.getOption("DB_VERSION");
|
||||
if (dbVersion !== "" && parseInt(dbVersion) !== DB_VERSION) {
|
||||
await this.clearStorage();
|
||||
this.reset();
|
||||
await this._initialize(workspaceSlug);
|
||||
return false;
|
||||
}
|
||||
|
||||
log(
|
||||
"OPFS is available, created persisted database at",
|
||||
openResponse.result.filename.replace(/^file:(.*?)\?vfs=opfs$/, "$1")
|
||||
);
|
||||
this.status = "ready";
|
||||
// Your SQLite code here.
|
||||
await createTables();
|
||||
|
||||
await this.setOption("DB_VERSION", DB_VERSION.toString());
|
||||
} catch (err) {
|
||||
logError(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.status = "error";
|
||||
throw new Error(`Failed to initialize database worker: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
syncWorkspace = async () => {
|
||||
if (document.hidden || !rootStore.user.localDBEnabled) return; // return if the window gets hidden
|
||||
await Sentry.startSpan({ name: "LOAD_WS", attributes: { slug: this.workspaceSlug } }, async () => {
|
||||
if (!rootStore.user.localDBEnabled) return;
|
||||
const syncInProgress = await this.getIsWriteInProgress("sync_workspace");
|
||||
if (syncInProgress) {
|
||||
log("Sync in progress, skipping");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await startSpan({ name: "LOAD_WS", attributes: { slug: this.workspaceSlug } }, async () => {
|
||||
this.setOption("sync_workspace", new Date().toUTCString());
|
||||
await loadWorkSpaceData(this.workspaceSlug);
|
||||
this.deleteOption("sync_workspace");
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
this.deleteOption("sync_workspace");
|
||||
}
|
||||
};
|
||||
|
||||
syncProject = async (projectId: string) => {
|
||||
if (document.hidden || !rootStore.user.localDBEnabled) return false; // return if the window gets hidden
|
||||
if (
|
||||
// document.hidden ||
|
||||
!rootStore.user.localDBEnabled
|
||||
)
|
||||
return false; // return if the window gets hidden
|
||||
|
||||
// Load labels, members, states, modules, cycles
|
||||
await this.syncIssues(projectId);
|
||||
|
|
@ -173,10 +161,11 @@ export class Storage {
|
|||
};
|
||||
|
||||
syncIssues = async (projectId: string) => {
|
||||
if (document.hidden || !rootStore.user.localDBEnabled) return false; // return if the window gets hidden
|
||||
|
||||
if (!rootStore.user.localDBEnabled || !this.db) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const sync = Sentry.startSpan({ name: `SYNC_ISSUES` }, () => this._syncIssues(projectId));
|
||||
const sync = startSpan({ name: `SYNC_ISSUES` }, () => this._syncIssues(projectId));
|
||||
this.setSync(projectId, sync);
|
||||
await sync;
|
||||
} catch (e) {
|
||||
|
|
@ -186,12 +175,12 @@ export class Storage {
|
|||
};
|
||||
|
||||
_syncIssues = async (projectId: string) => {
|
||||
const activeSpan = Sentry.getActiveSpan();
|
||||
const activeSpan = getActiveSpan();
|
||||
|
||||
log("### Sync started");
|
||||
let status = this.getStatus(projectId);
|
||||
if (status === "loading" || status === "syncing") {
|
||||
logInfo(`Project ${projectId} is already loading or syncing`);
|
||||
log(`Project ${projectId} is already loading or syncing`);
|
||||
return;
|
||||
}
|
||||
const syncPromise = this.getSync(projectId);
|
||||
|
|
@ -222,8 +211,8 @@ export class Storage {
|
|||
const issueService = new IssueService();
|
||||
|
||||
const response = await issueService.getIssuesForSync(this.workspaceSlug, projectId, queryParams);
|
||||
addIssuesBulk(response.results, BATCH_SIZE);
|
||||
|
||||
await addIssuesBulk(response.results, BATCH_SIZE);
|
||||
if (response.total_pages > 1) {
|
||||
const promiseArray = [];
|
||||
for (let i = 1; i < response.total_pages; i++) {
|
||||
|
|
@ -290,7 +279,7 @@ export class Storage {
|
|||
!rootStore.user.localDBEnabled
|
||||
) {
|
||||
if (rootStore.user.localDBEnabled) {
|
||||
logInfo(`Project ${projectId} is loading, falling back to server`);
|
||||
log(`Project ${projectId} is loading, falling back to server`);
|
||||
}
|
||||
const issueService = new IssueService();
|
||||
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries);
|
||||
|
|
@ -310,11 +299,9 @@ export class Storage {
|
|||
const issueService = new IssueService();
|
||||
return await issueService.getIssuesFromServer(workspaceSlug, projectId, queries);
|
||||
}
|
||||
// const issuesRaw = await runQuery(query);
|
||||
const end = performance.now();
|
||||
|
||||
const { total_count } = count[0];
|
||||
// const total_count = 2300;
|
||||
|
||||
const [pageSize, page, offset] = cursor.split(":");
|
||||
|
||||
|
|
@ -347,7 +334,6 @@ export class Storage {
|
|||
Parsing: parsingEnd - parsingStart,
|
||||
Grouping: groupingEnd - grouping,
|
||||
};
|
||||
log(issueResults);
|
||||
if ((window as any).DEBUG) {
|
||||
console.table(times);
|
||||
}
|
||||
|
|
@ -364,7 +350,7 @@ export class Storage {
|
|||
total_pages,
|
||||
};
|
||||
|
||||
const activeSpan = Sentry.getActiveSpan();
|
||||
const activeSpan = getActiveSpan();
|
||||
activeSpan?.setAttributes({
|
||||
projectId,
|
||||
count: total_count,
|
||||
|
|
@ -413,7 +399,7 @@ export class Storage {
|
|||
set(this.projectStatus, `${projectId}.issues.sync`, sync);
|
||||
};
|
||||
|
||||
getOption = async (key: string, fallback = "") => {
|
||||
getOption = async (key: string, fallback?: string | boolean | number) => {
|
||||
try {
|
||||
const options = await runQuery(`select * from options where key='${key}'`);
|
||||
if (options.length) {
|
||||
|
|
@ -429,6 +415,9 @@ export class Storage {
|
|||
await runQuery(`insert or replace into options (key, value) values ('${key}', '${value}')`);
|
||||
};
|
||||
|
||||
deleteOption = async (key: string) => {
|
||||
await runQuery(` DELETE FROM options where key='${key}'`);
|
||||
};
|
||||
getOptions = async (keys: string[]) => {
|
||||
const options = await runQuery(`select * from options where key in ('${keys.join("','")}')`);
|
||||
return options.reduce((acc: any, option: any) => {
|
||||
|
|
@ -436,6 +425,21 @@ export class Storage {
|
|||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
getIsWriteInProgress = async (op: string) => {
|
||||
const writeStartTime = await this.getOption(op, false);
|
||||
if (writeStartTime) {
|
||||
// Check if it has been more than 5seconds
|
||||
const current = new Date();
|
||||
const start = new Date(writeStartTime);
|
||||
|
||||
if (current.getTime() - start.getTime() < 5000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export const persistence = new Storage();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { persistence } from "../storage.sqlite";
|
||||
import { log } from "./utils";
|
||||
|
||||
const log = console.log;
|
||||
export const createIssueIndexes = async () => {
|
||||
const columns = [
|
||||
"state_id",
|
||||
|
|
@ -34,10 +34,8 @@ export const createWorkSpaceIndexes = async () => {
|
|||
const promises: Promise<any>[] = [];
|
||||
// Labels
|
||||
promises.push(persistence.db.exec({ sql: `CREATE INDEX labels_name_idx ON labels (id,name,project_id)` }));
|
||||
|
||||
// Modules
|
||||
promises.push(persistence.db.exec({ sql: `CREATE INDEX modules_name_idx ON modules (id,name,project_id)` }));
|
||||
|
||||
// States
|
||||
promises.push(persistence.db.exec({ sql: `CREATE INDEX states_name_idx ON states (id,name,project_id)` }));
|
||||
// Cycles
|
||||
|
|
@ -49,7 +47,7 @@ export const createWorkSpaceIndexes = async () => {
|
|||
// Estimate Points @todo
|
||||
promises.push(persistence.db.exec({ sql: `CREATE INDEX estimate_points_name_idx ON estimate_points (id,value)` }));
|
||||
// Options
|
||||
promises.push(persistence.db.exec({ sql: `CREATE INDEX options_name_idx ON options (name)` }));
|
||||
promises.push(persistence.db.exec({ sql: `CREATE INDEX options_key_idx ON options (key)` }));
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,47 +4,53 @@ import { IssueService } from "@/services/issue";
|
|||
import { persistence } from "../storage.sqlite";
|
||||
import { ARRAY_FIELDS, PRIORITY_MAP } from "./constants";
|
||||
import { issueSchema } from "./schemas";
|
||||
import { log } from "./utils";
|
||||
|
||||
export const PROJECT_OFFLINE_STATUS: Record<string, boolean> = {};
|
||||
|
||||
export const addIssue = async (issue: any) => {
|
||||
if (document.hidden || !rootStore.user.localDBEnabled) return;
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
stageIssueInserts(issue);
|
||||
persistence.db.exec("COMMIT;");
|
||||
if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
|
||||
await persistence.db.exec("BEGIN;");
|
||||
await stageIssueInserts(issue);
|
||||
await persistence.db.exec("COMMIT;");
|
||||
};
|
||||
|
||||
export const addIssuesBulk = async (issues: any, batchSize = 100) => {
|
||||
if (!rootStore.user.localDBEnabled) return;
|
||||
if (!rootStore.user.localDBEnabled || !persistence.db) return;
|
||||
|
||||
const insertStart = performance.now();
|
||||
await persistence.db.exec("BEGIN;");
|
||||
|
||||
for (let i = 0; i < issues.length; i += batchSize) {
|
||||
const batch = issues.slice(i, i + batchSize);
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
batch.forEach((issue: any) => {
|
||||
for (let j = 0; j < batch.length; j++) {
|
||||
const issue = batch[j];
|
||||
if (!issue.type_id) {
|
||||
issue.type_id = "";
|
||||
}
|
||||
stageIssueInserts(issue);
|
||||
});
|
||||
await persistence.db.exec("COMMIT;");
|
||||
await stageIssueInserts(issue);
|
||||
}
|
||||
}
|
||||
await persistence.db.exec("COMMIT;");
|
||||
|
||||
const insertEnd = performance.now();
|
||||
log("Inserted issues in ", `${insertEnd - insertStart}ms`);
|
||||
};
|
||||
export const deleteIssueFromLocal = async (issue_id: any) => {
|
||||
if (!rootStore.user.localDBEnabled) return;
|
||||
if (!rootStore.user.localDBEnabled || !persistence.db) return;
|
||||
|
||||
const deleteQuery = `delete from issues where id='${issue_id}'`;
|
||||
const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`;
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
persistence.db.exec("BEGIN;");
|
||||
persistence.db.exec(deleteQuery);
|
||||
persistence.db.exec(deleteMetaQuery);
|
||||
persistence.db.exec("COMMIT;");
|
||||
};
|
||||
// @todo: Update deletes the issue description from local. Implement a separate update.
|
||||
export const updateIssue = async (issue: TIssue & { is_local_update: number }) => {
|
||||
if (document.hidden || !rootStore.user.localDBEnabled) return;
|
||||
if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
|
||||
|
||||
const issue_id = issue.id;
|
||||
// delete the issue and its meta data
|
||||
|
|
@ -53,7 +59,7 @@ export const updateIssue = async (issue: TIssue & { is_local_update: number }) =
|
|||
};
|
||||
|
||||
export const syncDeletesToLocal = async (workspaceId: string, projectId: string, queries: any) => {
|
||||
if (!rootStore.user.localDBEnabled) return;
|
||||
if (!rootStore.user.localDBEnabled || !persistence.db) return;
|
||||
|
||||
const issueService = new IssueService();
|
||||
const response = await issueService.getDeletedIssues(workspaceId, projectId, queries);
|
||||
|
|
@ -62,7 +68,7 @@ export const syncDeletesToLocal = async (workspaceId: string, projectId: string,
|
|||
}
|
||||
};
|
||||
|
||||
const stageIssueInserts = (issue: any) => {
|
||||
const stageIssueInserts = async (issue: any) => {
|
||||
const issue_id = issue.id;
|
||||
issue.priority_proxy = PRIORITY_MAP[issue.priority as keyof typeof PRIORITY_MAP];
|
||||
|
||||
|
|
@ -94,7 +100,7 @@ const stageIssueInserts = (issue: any) => {
|
|||
const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${values});`;
|
||||
persistence.db.exec(query);
|
||||
|
||||
persistence.db.exec({
|
||||
await persistence.db.exec({
|
||||
sql: `DELETE from issue_meta where issue_id='${issue_id}'`,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,18 @@ import { ModuleService } from "@/services/module.service";
|
|||
import { ProjectStateService } from "@/services/project";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { persistence } from "../storage.sqlite";
|
||||
import { cycleSchema, estimatePointSchema, labelSchema, memberSchema, Schema, stateSchema } from "./schemas";
|
||||
import {
|
||||
cycleSchema,
|
||||
estimatePointSchema,
|
||||
labelSchema,
|
||||
memberSchema,
|
||||
moduleSchema,
|
||||
Schema,
|
||||
stateSchema,
|
||||
} from "./schemas";
|
||||
import { log } from "./utils";
|
||||
|
||||
const stageInserts = (table: string, schema: Schema, data: any) => {
|
||||
const stageInserts = async (table: string, schema: Schema, data: any) => {
|
||||
const keys = Object.keys(schema);
|
||||
// Pick only the keys that are in the schema
|
||||
const filteredData = keys.reduce((acc: any, key) => {
|
||||
|
|
@ -36,67 +45,46 @@ const stageInserts = (table: string, schema: Schema, data: any) => {
|
|||
})
|
||||
.join(", ");
|
||||
const query = `INSERT OR REPLACE INTO ${table} (${columns}) VALUES (${values});`;
|
||||
persistence.db.exec(query);
|
||||
await persistence.db.exec(query);
|
||||
};
|
||||
|
||||
export const loadLabels = async (workspaceSlug: string, batchSize = 500) => {
|
||||
const batchInserts = async (data: any[], table: string, schema: Schema, batchSize = 500) => {
|
||||
for (let i = 0; i < data.length; i += batchSize) {
|
||||
const batch = data.slice(i, i + batchSize);
|
||||
for (let j = 0; j < batch.length; j++) {
|
||||
const item = batch[j];
|
||||
await stageInserts(table, schema, item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getLabels = async (workspaceSlug: string) => {
|
||||
const issueLabelService = new IssueLabelService();
|
||||
const objects = await issueLabelService.getWorkspaceIssueLabels(workspaceSlug);
|
||||
for (let i = 0; i < objects.length; i += batchSize) {
|
||||
const batch = objects.slice(i, i + batchSize);
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
batch.forEach((label: any) => {
|
||||
stageInserts("labels", labelSchema, label);
|
||||
});
|
||||
await persistence.db.exec("COMMIT;");
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
export const loadModules = async (workspaceSlug: string, batchSize = 500) => {
|
||||
export const getModules = async (workspaceSlug: string) => {
|
||||
const moduleService = new ModuleService();
|
||||
const objects = await moduleService.getWorkspaceModules(workspaceSlug);
|
||||
for (let i = 0; i < objects.length; i += batchSize) {
|
||||
const batch = objects.slice(i, i + batchSize);
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
batch.forEach((label: any) => {
|
||||
stageInserts("modules", labelSchema, label);
|
||||
});
|
||||
await persistence.db.exec("COMMIT;");
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
export const loadCycles = async (workspaceSlug: string, batchSize = 500) => {
|
||||
export const getCycles = async (workspaceSlug: string) => {
|
||||
const cycleService = new CycleService();
|
||||
|
||||
const objects = await cycleService.getWorkspaceCycles(workspaceSlug);
|
||||
for (let i = 0; i < objects.length; i += batchSize) {
|
||||
const batch = objects.slice(i, i + batchSize);
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
batch.forEach((cycle: any) => {
|
||||
stageInserts("cycles", cycleSchema, cycle);
|
||||
});
|
||||
await persistence.db.exec("COMMIT;");
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
export const loadStates = async (workspaceSlug: string, batchSize = 500) => {
|
||||
export const getStates = async (workspaceSlug: string) => {
|
||||
const stateService = new ProjectStateService();
|
||||
const objects = await stateService.getWorkspaceStates(workspaceSlug);
|
||||
for (let i = 0; i < objects.length; i += batchSize) {
|
||||
const batch = objects.slice(i, i + batchSize);
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
batch.forEach((state: any) => {
|
||||
stageInserts("states", stateSchema, state);
|
||||
});
|
||||
await persistence.db.exec("COMMIT;");
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
export const loadEstimatePoints = async (workspaceSlug: string, batchSize = 500) => {
|
||||
export const getEstimatePoints = async (workspaceSlug: string) => {
|
||||
const estimateService = new EstimateService();
|
||||
const estimates = await estimateService.fetchWorkspaceEstimates(workspaceSlug);
|
||||
const objects: IEstimatePoint[] = [];
|
||||
|
|
@ -105,38 +93,36 @@ export const loadEstimatePoints = async (workspaceSlug: string, batchSize = 500)
|
|||
objects.concat(estimate.points);
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < objects.length; i += batchSize) {
|
||||
const batch = objects.slice(i, i + batchSize);
|
||||
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
batch.forEach((point: any) => {
|
||||
stageInserts("estimate_points", estimatePointSchema, point);
|
||||
});
|
||||
await persistence.db.exec("COMMIT;");
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
export const loadMembers = async (workspaceSlug: string, batchSize = 500) => {
|
||||
export const getMembers = async (workspaceSlug: string) => {
|
||||
const workspaceService = new WorkspaceService(API_BASE_URL);
|
||||
const members = await workspaceService.fetchWorkspaceMembers(workspaceSlug);
|
||||
const objects = members.map((member: IWorkspaceMember) => member.member);
|
||||
for (let i = 0; i < objects.length; i += batchSize) {
|
||||
const batch = objects.slice(i, i + batchSize);
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
batch.forEach((member: any) => {
|
||||
stageInserts("members", memberSchema, member);
|
||||
});
|
||||
await persistence.db.exec("COMMIT;");
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
export const loadWorkSpaceData = async (workspaceSlug: string) => {
|
||||
log("Loading workspace data");
|
||||
const promises = [];
|
||||
promises.push(loadLabels(workspaceSlug));
|
||||
promises.push(loadModules(workspaceSlug));
|
||||
promises.push(loadCycles(workspaceSlug));
|
||||
promises.push(loadStates(workspaceSlug));
|
||||
promises.push(loadEstimatePoints(workspaceSlug));
|
||||
promises.push(loadMembers(workspaceSlug));
|
||||
await Promise.all(promises);
|
||||
promises.push(getLabels(workspaceSlug));
|
||||
promises.push(getModules(workspaceSlug));
|
||||
promises.push(getCycles(workspaceSlug));
|
||||
promises.push(getStates(workspaceSlug));
|
||||
promises.push(getEstimatePoints(workspaceSlug));
|
||||
promises.push(getMembers(workspaceSlug));
|
||||
const [labels, modules, cycles, states, estimates, memebers] = await Promise.all(promises);
|
||||
|
||||
const start = performance.now();
|
||||
await persistence.db.exec("BEGIN TRANSACTION;");
|
||||
await batchInserts(labels, "labels", labelSchema);
|
||||
await batchInserts(modules, "modules", moduleSchema);
|
||||
await batchInserts(cycles, "cycles", cycleSchema);
|
||||
await batchInserts(states, "states", stateSchema);
|
||||
await batchInserts(estimates, "estimate_points", estimatePointSchema);
|
||||
await batchInserts(memebers, "members", memberSchema);
|
||||
await persistence.db.exec("COMMIT");
|
||||
const end = performance.now();
|
||||
log("Time taken to load workspace data", end - start);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
|||
per_page,
|
||||
group_by,
|
||||
sub_group_by,
|
||||
order_by = "created_at",
|
||||
order_by = "-created_at",
|
||||
...otherProps
|
||||
} = translateQueryParams(queries);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
// import { SQL } from "./sqlite";
|
||||
|
||||
import { persistence } from "../storage.sqlite";
|
||||
|
||||
export const runQuery = async (sql: string) => {
|
||||
const data = await persistence.db.exec({
|
||||
const data = await persistence.db?.exec({
|
||||
sql,
|
||||
rowMode: "object",
|
||||
returnValue: "resultRows",
|
||||
});
|
||||
|
||||
return data.result.resultRows;
|
||||
return data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,8 +4,20 @@ import { issueSchema } from "./schemas";
|
|||
import { wrapDateTime } from "./utils";
|
||||
|
||||
export const translateQueryParams = (queries: any) => {
|
||||
const { group_by, sub_group_by, labels, assignees, state, cycle, module, priority, type, issue_type, ...otherProps } =
|
||||
queries;
|
||||
const {
|
||||
group_by,
|
||||
layout,
|
||||
sub_group_by,
|
||||
labels,
|
||||
assignees,
|
||||
state,
|
||||
cycle,
|
||||
module,
|
||||
priority,
|
||||
type,
|
||||
issue_type,
|
||||
...otherProps
|
||||
} = queries;
|
||||
|
||||
const order_by = queries.order_by;
|
||||
if (state) otherProps.state_id = state;
|
||||
|
|
@ -33,7 +45,7 @@ export const translateQueryParams = (queries: any) => {
|
|||
}
|
||||
|
||||
// Fix invalid orderby when switching from spreadsheet layout
|
||||
if ((group_by || sub_group_by) && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
if (layout === "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
otherProps.order_by = "sort_order";
|
||||
}
|
||||
// For each property value, replace None with empty string
|
||||
|
|
|
|||
|
|
@ -24,17 +24,18 @@ const createTableSQLfromSchema = (tableName: string, schema: Schema) => {
|
|||
};
|
||||
|
||||
export const createTables = async () => {
|
||||
persistence.db.exec("BEGIN TRANSACTION;");
|
||||
//@todo use promise.all or send all statements in one go
|
||||
await persistence.db.exec("BEGIN;");
|
||||
|
||||
persistence.db.exec(createTableSQLfromSchema("issues", issueSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("issue_meta", issueMetaSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("modules", moduleSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("labels", labelSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("states", stateSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("cycles", cycleSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("estimate_points", estimatePointSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("members", memberSchema));
|
||||
persistence.db.exec(createTableSQLfromSchema("options", optionsSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("issues", issueSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("issue_meta", issueMetaSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("modules", moduleSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("labels", labelSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("states", stateSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("cycles", cycleSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("estimate_points", estimatePointSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("members", memberSchema));
|
||||
await persistence.db.exec(createTableSQLfromSchema("options", optionsSchema));
|
||||
|
||||
persistence.db.exec("COMMIT;");
|
||||
await persistence.db.exec("COMMIT;");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
import { captureException } from "@sentry/nextjs";
|
||||
import pick from "lodash/pick";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
|
|
@ -14,8 +14,8 @@ export const logError = (e: any) => {
|
|||
if (e?.result?.errorClass === "SQLite3Error") {
|
||||
e = parseSQLite3Error(e);
|
||||
}
|
||||
Sentry.captureException(e);
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
captureException(e);
|
||||
};
|
||||
export const logInfo = console.info;
|
||||
|
||||
|
|
@ -152,3 +152,25 @@ const parseSQLite3Error = (error: any) => {
|
|||
error.result = JSON.stringify(error.result);
|
||||
return error;
|
||||
};
|
||||
|
||||
export const clearOPFS = async () => {
|
||||
const storageManager = window.navigator.storage;
|
||||
const fileSystemDirectoryHandle = await storageManager.getDirectory();
|
||||
const userAgent = navigator.userAgent;
|
||||
const isChrome = userAgent.includes("Chrome") && !userAgent.includes("Edg") && !userAgent.includes("OPR");
|
||||
|
||||
if (isChrome) {
|
||||
await (fileSystemDirectoryHandle as any).remove({ recursive: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await (fileSystemDirectoryHandle as any).entries();
|
||||
for await (const entry of entries) {
|
||||
const [name] = entry;
|
||||
try {
|
||||
await fileSystemDirectoryHandle.removeEntry(name);
|
||||
} catch (e) {
|
||||
console.log("Error", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
114
web/core/local-db/worker/db.ts
Normal file
114
web/core/local-db/worker/db.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import * as Comlink from "comlink";
|
||||
import { OPFSCoopSyncVFS as MyVFS } from "./wa-sqlite/src/OPFSCoopSyncVFS";
|
||||
import * as SQLite from "./wa-sqlite/src/sqlite-api";
|
||||
import SQLiteESMFactory from "./wa-sqlite/src/wa-sqlite.mjs";
|
||||
|
||||
type TQueryProps = {
|
||||
sql: string;
|
||||
rowMode: string;
|
||||
returnValue: string;
|
||||
bind: any[];
|
||||
};
|
||||
const mergeToObject = (columns: string[], row: any[]) => {
|
||||
const obj: any = {};
|
||||
columns.forEach((column, index) => {
|
||||
obj[column] = row[index];
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
interface SQLiteInstance {
|
||||
db: unknown;
|
||||
exec: (sql: string) => Promise<unknown[]>;
|
||||
}
|
||||
|
||||
export class DBClass {
|
||||
private instance: SQLiteInstance = {} as SQLiteInstance;
|
||||
private sqlite3: any;
|
||||
private tp: Promise<any> | null = null;
|
||||
private tpResolver: any;
|
||||
async init(dbName: string) {
|
||||
if (!dbName || typeof dbName !== "string") {
|
||||
throw new Error("Invalid database name");
|
||||
}
|
||||
|
||||
try {
|
||||
const m = await SQLiteESMFactory();
|
||||
this.sqlite3 = SQLite.Factory(m);
|
||||
const vfs = await MyVFS.create("plane", m);
|
||||
this.sqlite3.vfs_register(vfs, true);
|
||||
const db = await this.sqlite3.open_v2(`${dbName}.sqlite3`);
|
||||
this.instance.db = db;
|
||||
this.instance.exec = async (sql: string) => {
|
||||
const rows: any[] = [];
|
||||
await this.sqlite3.exec(db, sql, (row: any[], columns: string[]) => {
|
||||
rows.push(mergeToObject(columns, row));
|
||||
});
|
||||
|
||||
return rows;
|
||||
};
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize database: ${(error as any)?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
runQuery(sql: string) {
|
||||
return this.instance.exec(sql);
|
||||
}
|
||||
|
||||
async exec(props: string | TQueryProps) {
|
||||
if (this.tp && props === "BEGIN;") {
|
||||
await this.tp;
|
||||
}
|
||||
let sql: string, bind: any[];
|
||||
if (typeof props === "string") {
|
||||
sql = props;
|
||||
} else {
|
||||
({ sql, bind } = props);
|
||||
if (bind) {
|
||||
for await (const stmt of this.sqlite3.statements(this.instance.db, sql)) {
|
||||
bind.forEach((b, i) => {
|
||||
this.sqlite3.bind(stmt, i + 1, b);
|
||||
});
|
||||
|
||||
const rows = [];
|
||||
|
||||
do {
|
||||
const columns = await this.sqlite3.column_names(stmt);
|
||||
const row = await this.sqlite3.row(stmt);
|
||||
rows.push(mergeToObject(columns, row));
|
||||
} while ((await this.sqlite3.step(stmt)) === SQLite.SQLITE_ROW);
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sql === "BEGIN;") {
|
||||
this.tp = new Promise((resolve, reject) => {
|
||||
this.tpResolver = { resolve, reject };
|
||||
});
|
||||
}
|
||||
|
||||
if (sql === "COMMIT;" && this.tp) {
|
||||
await this.instance.exec(sql);
|
||||
this.tpResolver.resolve();
|
||||
this.tp = null;
|
||||
return;
|
||||
}
|
||||
return await this.instance.exec(sql);
|
||||
}
|
||||
async close() {
|
||||
try {
|
||||
if (!this.instance.db) {
|
||||
return;
|
||||
}
|
||||
await this.sqlite3.close(this.instance.db);
|
||||
// Clear instance to prevent usage after closing
|
||||
this.instance = {} as SQLiteInstance;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to close database: ${(error as any)?.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Comlink.expose(DBClass);
|
||||
508
web/core/local-db/worker/wa-sqlite/src/FacadeVFS.js
Normal file
508
web/core/local-db/worker/wa-sqlite/src/FacadeVFS.js
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
|
||||
import * as VFS from './VFS.js';
|
||||
|
||||
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
||||
|
||||
// Convenience base class for a JavaScript VFS.
|
||||
// The raw xOpen, xRead, etc. function signatures receive only C primitives
|
||||
// which aren't easy to work with. This class provides corresponding calls
|
||||
// like jOpen, jRead, etc., which receive JavaScript-friendlier arguments
|
||||
// such as string, Uint8Array, and DataView.
|
||||
export class FacadeVFS extends VFS.Base {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {object} module
|
||||
*/
|
||||
constructor(name, module) {
|
||||
super(name, module);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to indicate which methods are asynchronous.
|
||||
* @param {string} methodName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAsyncMethod(methodName) {
|
||||
// The input argument is a string like "xOpen", so convert to "jOpen".
|
||||
// Then check if the method exists and is async.
|
||||
const jMethodName = `j${methodName.slice(1)}`;
|
||||
return this[jMethodName] instanceof AsyncFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filename for a file id for use by mixins.
|
||||
* @param {number} pFile
|
||||
* @returns {string}
|
||||
*/
|
||||
getFilename(pFile) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string?} filename
|
||||
* @param {number} pFile
|
||||
* @param {number} flags
|
||||
* @param {DataView} pOutFlags
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jOpen(filename, pFile, flags, pOutFlags) {
|
||||
return VFS.SQLITE_CANTOPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {number} syncDir
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jDelete(filename, syncDir) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {number} flags
|
||||
* @param {DataView} pResOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jAccess(filename, flags, pResOut) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {Uint8Array} zOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jFullPathname(filename, zOut) {
|
||||
// Copy the filename to the output buffer.
|
||||
const { read, written } = new TextEncoder().encodeInto(filename, zOut);
|
||||
if (read < filename.length) return VFS.SQLITE_IOERR;
|
||||
if (written >= zOut.length) return VFS.SQLITE_IOERR;
|
||||
zOut[written] = 0;
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} zBuf
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jGetLastError(zBuf) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jClose(pFile) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {Uint8Array} pData
|
||||
* @param {number} iOffset
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jRead(pFile, pData, iOffset) {
|
||||
pData.fill(0);
|
||||
return VFS.SQLITE_IOERR_SHORT_READ;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {Uint8Array} pData
|
||||
* @param {number} iOffset
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jWrite(pFile, pData, iOffset) {
|
||||
return VFS.SQLITE_IOERR_WRITE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} size
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jTruncate(pFile, size) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} flags
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jSync(pFile, flags) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {DataView} pSize
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jFileSize(pFile, pSize) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} lockType
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jLock(pFile, lockType) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} lockType
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jUnlock(pFile, lockType) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {DataView} pResOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jCheckReservedLock(pFile, pResOut) {
|
||||
pResOut.setInt32(0, 0, true);
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} op
|
||||
* @param {DataView} pArg
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jFileControl(pFile, op, pArg) {
|
||||
return VFS.SQLITE_NOTFOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jSectorSize(pFile) {
|
||||
return super.xSectorSize(pFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jDeviceCharacteristics(pFile) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} pFile
|
||||
* @param {number} flags
|
||||
* @param {number} pOutFlags
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xOpen(pVfs, zName, pFile, flags, pOutFlags) {
|
||||
const filename = this.#decodeFilename(zName, flags);
|
||||
const pOutFlagsView = this.#makeTypedDataView('Int32', pOutFlags);
|
||||
this['log']?.('jOpen', filename, pFile, '0x' + flags.toString(16));
|
||||
return this.jOpen(filename, pFile, flags, pOutFlagsView);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} syncDir
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xDelete(pVfs, zName, syncDir) {
|
||||
const filename = this._module.UTF8ToString(zName);
|
||||
this['log']?.('jDelete', filename, syncDir);
|
||||
return this.jDelete(filename, syncDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} flags
|
||||
* @param {number} pResOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xAccess(pVfs, zName, flags, pResOut) {
|
||||
const filename = this._module.UTF8ToString(zName);
|
||||
const pResOutView = this.#makeTypedDataView('Int32', pResOut);
|
||||
this['log']?.('jAccess', filename, flags);
|
||||
return this.jAccess(filename, flags, pResOutView);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} nOut
|
||||
* @param {number} zOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xFullPathname(pVfs, zName, nOut, zOut) {
|
||||
const filename = this._module.UTF8ToString(zName);
|
||||
const zOutArray = this._module.HEAPU8.subarray(zOut, zOut + nOut);
|
||||
this['log']?.('jFullPathname', filename, nOut);
|
||||
return this.jFullPathname(filename, zOutArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} nBuf
|
||||
* @param {number} zBuf
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xGetLastError(pVfs, nBuf, zBuf) {
|
||||
const zBufArray = this._module.HEAPU8.subarray(zBuf, zBuf + nBuf);
|
||||
this['log']?.('jGetLastError', nBuf);
|
||||
return this.jGetLastError(zBufArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xClose(pFile) {
|
||||
this['log']?.('jClose', pFile);
|
||||
return this.jClose(pFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} pData
|
||||
* @param {number} iAmt
|
||||
* @param {number} iOffsetLo
|
||||
* @param {number} iOffsetHi
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xRead(pFile, pData, iAmt, iOffsetLo, iOffsetHi) {
|
||||
const pDataArray = this.#makeDataArray(pData, iAmt);
|
||||
const iOffset = delegalize(iOffsetLo, iOffsetHi);
|
||||
this['log']?.('jRead', pFile, iAmt, iOffset);
|
||||
return this.jRead(pFile, pDataArray, iOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} pData
|
||||
* @param {number} iAmt
|
||||
* @param {number} iOffsetLo
|
||||
* @param {number} iOffsetHi
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xWrite(pFile, pData, iAmt, iOffsetLo, iOffsetHi) {
|
||||
const pDataArray = this.#makeDataArray(pData, iAmt);
|
||||
const iOffset = delegalize(iOffsetLo, iOffsetHi);
|
||||
this['log']?.('jWrite', pFile, pDataArray, iOffset);
|
||||
return this.jWrite(pFile, pDataArray, iOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} sizeLo
|
||||
* @param {number} sizeHi
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xTruncate(pFile, sizeLo, sizeHi) {
|
||||
const size = delegalize(sizeLo, sizeHi);
|
||||
this['log']?.('jTruncate', pFile, size);
|
||||
return this.jTruncate(pFile, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} flags
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xSync(pFile, flags) {
|
||||
this['log']?.('jSync', pFile, flags);
|
||||
return this.jSync(pFile, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} pFile
|
||||
* @param {number} pSize
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xFileSize(pFile, pSize) {
|
||||
const pSizeView = this.#makeTypedDataView('BigInt64', pSize);
|
||||
this['log']?.('jFileSize', pFile);
|
||||
return this.jFileSize(pFile, pSizeView);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} lockType
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xLock(pFile, lockType) {
|
||||
this['log']?.('jLock', pFile, lockType);
|
||||
return this.jLock(pFile, lockType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} lockType
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xUnlock(pFile, lockType) {
|
||||
this['log']?.('jUnlock', pFile, lockType);
|
||||
return this.jUnlock(pFile, lockType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} pResOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xCheckReservedLock(pFile, pResOut) {
|
||||
const pResOutView = this.#makeTypedDataView('Int32', pResOut);
|
||||
this['log']?.('jCheckReservedLock', pFile);
|
||||
return this.jCheckReservedLock(pFile, pResOutView);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} op
|
||||
* @param {number} pArg
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xFileControl(pFile, op, pArg) {
|
||||
const pArgView = new DataView(
|
||||
this._module.HEAPU8.buffer,
|
||||
this._module.HEAPU8.byteOffset + pArg);
|
||||
this['log']?.('jFileControl', pFile, op, pArgView);
|
||||
return this.jFileControl(pFile, op, pArgView);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xSectorSize(pFile) {
|
||||
this['log']?.('jSectorSize', pFile);
|
||||
return this.jSectorSize(pFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xDeviceCharacteristics(pFile) {
|
||||
this['log']?.('jDeviceCharacteristics', pFile);
|
||||
return this.jDeviceCharacteristics(pFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapped DataView for pointer arguments.
|
||||
* Pointers to a single value are passed using DataView. A Proxy
|
||||
* wrapper prevents use of incorrect type or endianness.
|
||||
* @param {'Int32'|'BigInt64'} type
|
||||
* @param {number} byteOffset
|
||||
* @returns {DataView}
|
||||
*/
|
||||
#makeTypedDataView(type, byteOffset) {
|
||||
const byteLength = type === 'Int32' ? 4 : 8;
|
||||
const getter = `get${type}`;
|
||||
const setter = `set${type}`;
|
||||
const makeDataView = () => new DataView(
|
||||
this._module.HEAPU8.buffer,
|
||||
this._module.HEAPU8.byteOffset + byteOffset,
|
||||
byteLength);
|
||||
let dataView = makeDataView();
|
||||
return new Proxy(dataView, {
|
||||
get(_, prop) {
|
||||
if (dataView.buffer.byteLength === 0) {
|
||||
// WebAssembly memory resize detached the buffer.
|
||||
dataView = makeDataView();
|
||||
}
|
||||
if (prop === getter) {
|
||||
return function(byteOffset, littleEndian) {
|
||||
if (!littleEndian) throw new Error('must be little endian');
|
||||
return dataView[prop](byteOffset, littleEndian);
|
||||
}
|
||||
}
|
||||
if (prop === setter) {
|
||||
return function(byteOffset, value, littleEndian) {
|
||||
if (!littleEndian) throw new Error('must be little endian');
|
||||
return dataView[prop](byteOffset, value, littleEndian);
|
||||
}
|
||||
}
|
||||
if (typeof prop === 'string' && (prop.match(/^(get)|(set)/))) {
|
||||
throw new Error('invalid type');
|
||||
}
|
||||
const result = dataView[prop];
|
||||
return typeof result === 'function' ? result.bind(dataView) : result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} byteOffset
|
||||
* @param {number} byteLength
|
||||
*/
|
||||
#makeDataArray(byteOffset, byteLength) {
|
||||
let target = this._module.HEAPU8.subarray(byteOffset, byteOffset + byteLength);
|
||||
return new Proxy(target, {
|
||||
get: (_, prop, receiver) => {
|
||||
if (target.buffer.byteLength === 0) {
|
||||
// WebAssembly memory resize detached the buffer.
|
||||
target = this._module.HEAPU8.subarray(byteOffset, byteOffset + byteLength);
|
||||
}
|
||||
const result = target[prop];
|
||||
return typeof result === 'function' ? result.bind(target) : result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#decodeFilename(zName, flags) {
|
||||
if (flags & VFS.SQLITE_OPEN_URI) {
|
||||
// The first null-terminated string is the URI path. Subsequent
|
||||
// strings are query parameter keys and values.
|
||||
// https://www.sqlite.org/c3ref/open.html#urifilenamesinsqlite3open
|
||||
let pName = zName;
|
||||
let state = 1;
|
||||
const charCodes = [];
|
||||
while (state) {
|
||||
const charCode = this._module.HEAPU8[pName++];
|
||||
if (charCode) {
|
||||
charCodes.push(charCode);
|
||||
} else {
|
||||
if (!this._module.HEAPU8[pName]) state = null;
|
||||
switch (state) {
|
||||
case 1: // path
|
||||
charCodes.push('?'.charCodeAt(0));
|
||||
state = 2;
|
||||
break;
|
||||
case 2: // key
|
||||
charCodes.push('='.charCodeAt(0));
|
||||
state = 3;
|
||||
break;
|
||||
case 3: // value
|
||||
charCodes.push('&'.charCodeAt(0));
|
||||
state = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new TextDecoder().decode(new Uint8Array(charCodes));
|
||||
}
|
||||
return zName ? this._module.UTF8ToString(zName) : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Emscripten "legalizes" 64-bit integer arguments by passing them as
|
||||
// two 32-bit signed integers.
|
||||
function delegalize(lo32, hi32) {
|
||||
return (hi32 * 0x100000000) + lo32 + (lo32 < 0 ? 2**32 : 0);
|
||||
}
|
||||
592
web/core/local-db/worker/wa-sqlite/src/OPFSCoopSyncVFS.js
Normal file
592
web/core/local-db/worker/wa-sqlite/src/OPFSCoopSyncVFS.js
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
|
||||
import { FacadeVFS } from "./FacadeVFS.js";
|
||||
import * as VFS from "./VFS.js";
|
||||
|
||||
const DEFAULT_TEMPORARY_FILES = 10;
|
||||
const LOCK_NOTIFY_INTERVAL = 1000;
|
||||
|
||||
const DB_RELATED_FILE_SUFFIXES = ["", "-journal", "-wal"];
|
||||
|
||||
const finalizationRegistry = new FinalizationRegistry((releaser) => releaser());
|
||||
|
||||
class File {
|
||||
/** @type {string} */ path;
|
||||
/** @type {number} */ flags;
|
||||
/** @type {FileSystemSyncAccessHandle} */ accessHandle;
|
||||
|
||||
/** @type {PersistentFile?} */ persistentFile;
|
||||
|
||||
constructor(path, flags) {
|
||||
this.path = path;
|
||||
this.flags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
class PersistentFile {
|
||||
/** @type {FileSystemFileHandle} */ fileHandle;
|
||||
/** @type {FileSystemSyncAccessHandle} */ accessHandle = null;
|
||||
|
||||
// The following properties are for main database files.
|
||||
|
||||
/** @type {boolean} */ isLockBusy = false;
|
||||
/** @type {boolean} */ isFileLocked = false;
|
||||
/** @type {boolean} */ isRequestInProgress = false;
|
||||
/** @type {function} */ handleLockReleaser = null;
|
||||
|
||||
/** @type {BroadcastChannel} */ handleRequestChannel;
|
||||
/** @type {boolean} */ isHandleRequested = false;
|
||||
|
||||
constructor(fileHandle) {
|
||||
this.fileHandle = fileHandle;
|
||||
}
|
||||
}
|
||||
|
||||
export class OPFSCoopSyncVFS extends FacadeVFS {
|
||||
/** @type {Map<number, File>} */ mapIdToFile = new Map();
|
||||
|
||||
lastError = null;
|
||||
log = null; //function(...args) { console.log(`[${contextName}]`, ...args) };
|
||||
|
||||
/** @type {Map<string, PersistentFile>} */ persistentFiles = new Map();
|
||||
/** @type {Map<string, FileSystemSyncAccessHandle>} */ boundAccessHandles = new Map();
|
||||
/** @type {Set<FileSystemSyncAccessHandle>} */ unboundAccessHandles = new Set();
|
||||
/** @type {Set<string>} */ accessiblePaths = new Set();
|
||||
releaser = null;
|
||||
|
||||
static async create(name, module) {
|
||||
const vfs = new OPFSCoopSyncVFS(name, module);
|
||||
await Promise.all([vfs.isReady(), vfs.#initialize(DEFAULT_TEMPORARY_FILES)]);
|
||||
return vfs;
|
||||
}
|
||||
|
||||
constructor(name, module) {
|
||||
super(name, module);
|
||||
}
|
||||
|
||||
async #initialize(nTemporaryFiles) {
|
||||
// Delete temporary directories no longer in use.
|
||||
const root = await navigator.storage.getDirectory();
|
||||
// @ts-ignore
|
||||
for await (const entry of root.values()) {
|
||||
if (entry.kind === "directory" && entry.name.startsWith(".ahp-")) {
|
||||
// A lock with the same name as the directory protects it from
|
||||
// being deleted.
|
||||
await navigator.locks.request(entry.name, { ifAvailable: true }, async (lock) => {
|
||||
if (lock) {
|
||||
this.log?.(`Deleting temporary directory ${entry.name}`);
|
||||
await root.removeEntry(entry.name, { recursive: true });
|
||||
} else {
|
||||
this.log?.(`Temporary directory ${entry.name} is in use`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create our temporary directory.
|
||||
const tmpDirName = `.ahp-${Math.random().toString(36).slice(2)}`;
|
||||
this.releaser = await new Promise((resolve) => {
|
||||
navigator.locks.request(tmpDirName, () => {
|
||||
return new Promise((release) => {
|
||||
resolve(release);
|
||||
});
|
||||
});
|
||||
});
|
||||
finalizationRegistry.register(this, this.releaser);
|
||||
const tmpDir = await root.getDirectoryHandle(tmpDirName, { create: true });
|
||||
|
||||
// Populate temporary directory.
|
||||
for (let i = 0; i < nTemporaryFiles; i++) {
|
||||
const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true });
|
||||
const tmpAccessHandle = await tmpFile.createSyncAccessHandle();
|
||||
this.unboundAccessHandles.add(tmpAccessHandle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string?} zName
|
||||
* @param {number} fileId
|
||||
* @param {number} flags
|
||||
* @param {DataView} pOutFlags
|
||||
* @returns {number}
|
||||
*/
|
||||
jOpen(zName, fileId, flags, pOutFlags) {
|
||||
try {
|
||||
const url = new URL(zName || Math.random().toString(36).slice(2), "file://");
|
||||
const path = url.pathname;
|
||||
|
||||
if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
||||
const persistentFile = this.persistentFiles.get(path);
|
||||
if (persistentFile?.isRequestInProgress) {
|
||||
// Should not reach here unless SQLite itself retries an open.
|
||||
// Otherwise, asynchronous operations started on a previous
|
||||
// open try should have completed.
|
||||
return VFS.SQLITE_BUSY;
|
||||
} else if (!persistentFile) {
|
||||
// This is the usual starting point for opening a database.
|
||||
// Register a Promise that resolves when the database and related
|
||||
// files are ready to be used.
|
||||
this.log?.(`creating persistent file for ${path}`);
|
||||
const create = !!(flags & VFS.SQLITE_OPEN_CREATE);
|
||||
this._module.retryOps.push(
|
||||
(async () => {
|
||||
try {
|
||||
// Get the path directory handle.
|
||||
let dirHandle = await navigator.storage.getDirectory();
|
||||
const directories = path.split("/").filter((d) => d);
|
||||
const filename = directories.pop();
|
||||
for (const directory of directories) {
|
||||
dirHandle = await dirHandle.getDirectoryHandle(directory, { create });
|
||||
}
|
||||
|
||||
// Get file handles for the database and related files,
|
||||
// and create persistent file instances.
|
||||
for (const suffix of DB_RELATED_FILE_SUFFIXES) {
|
||||
const fileHandle = await dirHandle.getFileHandle(filename + suffix, { create });
|
||||
await this.#createPersistentFile(fileHandle);
|
||||
}
|
||||
|
||||
// Get access handles for the files.
|
||||
const file = new File(path, flags);
|
||||
file.persistentFile = this.persistentFiles.get(path);
|
||||
await this.#requestAccessHandle(file);
|
||||
} catch (e) {
|
||||
// Use an invalid persistent file to signal this error
|
||||
// for the retried open.
|
||||
const persistentFile = new PersistentFile(null);
|
||||
this.persistentFiles.set(path, persistentFile);
|
||||
console.error(e);
|
||||
}
|
||||
})()
|
||||
);
|
||||
return VFS.SQLITE_BUSY;
|
||||
} else if (!persistentFile.fileHandle) {
|
||||
// The asynchronous open operation failed.
|
||||
this.persistentFiles.delete(path);
|
||||
return VFS.SQLITE_CANTOPEN;
|
||||
} else if (!persistentFile.accessHandle) {
|
||||
// This branch is reached if the database was previously opened
|
||||
// and closed.
|
||||
this._module.retryOps.push(
|
||||
(async () => {
|
||||
const file = new File(path, flags);
|
||||
file.persistentFile = this.persistentFiles.get(path);
|
||||
await this.#requestAccessHandle(file);
|
||||
})()
|
||||
);
|
||||
return VFS.SQLITE_BUSY;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.accessiblePaths.has(path) && !(flags & VFS.SQLITE_OPEN_CREATE)) {
|
||||
throw new Error(`File ${path} not found`);
|
||||
}
|
||||
|
||||
const file = new File(path, flags);
|
||||
this.mapIdToFile.set(fileId, file);
|
||||
|
||||
if (this.persistentFiles.has(path)) {
|
||||
file.persistentFile = this.persistentFiles.get(path);
|
||||
} else if (this.boundAccessHandles.has(path)) {
|
||||
// This temporary file was previously created and closed. Reopen
|
||||
// the same access handle.
|
||||
file.accessHandle = this.boundAccessHandles.get(path);
|
||||
} else if (this.unboundAccessHandles.size) {
|
||||
// Associate an unbound access handle to this file.
|
||||
file.accessHandle = this.unboundAccessHandles.values().next().value;
|
||||
file.accessHandle.truncate(0);
|
||||
this.unboundAccessHandles.delete(file.accessHandle);
|
||||
this.boundAccessHandles.set(path, file.accessHandle);
|
||||
}
|
||||
this.accessiblePaths.add(path);
|
||||
|
||||
pOutFlags.setInt32(0, flags, true);
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_CANTOPEN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} zName
|
||||
* @param {number} syncDir
|
||||
* @returns {number}
|
||||
*/
|
||||
jDelete(zName, syncDir) {
|
||||
try {
|
||||
const url = new URL(zName, "file://");
|
||||
const path = url.pathname;
|
||||
if (this.persistentFiles.has(path)) {
|
||||
const persistentFile = this.persistentFiles.get(path);
|
||||
persistentFile.accessHandle.truncate(0);
|
||||
} else {
|
||||
this.boundAccessHandles.get(path)?.truncate(0);
|
||||
}
|
||||
this.accessiblePaths.delete(path);
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_DELETE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} zName
|
||||
* @param {number} flags
|
||||
* @param {DataView} pResOut
|
||||
* @returns {number}
|
||||
*/
|
||||
jAccess(zName, flags, pResOut) {
|
||||
try {
|
||||
const url = new URL(zName, "file://");
|
||||
const path = url.pathname;
|
||||
pResOut.setInt32(0, this.accessiblePaths.has(path) ? 1 : 0, true);
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_ACCESS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @returns {number}
|
||||
*/
|
||||
jClose(fileId) {
|
||||
try {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
this.mapIdToFile.delete(fileId);
|
||||
|
||||
if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
||||
if (file.persistentFile?.handleLockReleaser) {
|
||||
this.#releaseAccessHandle(file);
|
||||
}
|
||||
} else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
||||
file.accessHandle.truncate(0);
|
||||
this.accessiblePaths.delete(file.path);
|
||||
if (!this.persistentFiles.has(file.path)) {
|
||||
this.boundAccessHandles.delete(file.path);
|
||||
this.unboundAccessHandles.add(file.accessHandle);
|
||||
}
|
||||
}
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_CLOSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {Uint8Array} pData
|
||||
* @param {number} iOffset
|
||||
* @returns {number}
|
||||
*/
|
||||
jRead(fileId, pData, iOffset) {
|
||||
try {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
|
||||
// On Chrome (at least), passing pData to accessHandle.read() is
|
||||
// an error because pData is a Proxy of a Uint8Array. Calling
|
||||
// subarray() produces a real Uint8Array and that works.
|
||||
const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
|
||||
const bytesRead = accessHandle.read(pData.subarray(), { at: iOffset });
|
||||
|
||||
// Opening a database file performs one read without a xLock call.
|
||||
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB && !file.persistentFile.isFileLocked) {
|
||||
this.#releaseAccessHandle(file);
|
||||
}
|
||||
|
||||
if (bytesRead < pData.byteLength) {
|
||||
pData.fill(0, bytesRead);
|
||||
return VFS.SQLITE_IOERR_SHORT_READ;
|
||||
}
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_READ;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {Uint8Array} pData
|
||||
* @param {number} iOffset
|
||||
* @returns {number}
|
||||
*/
|
||||
jWrite(fileId, pData, iOffset) {
|
||||
try {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
|
||||
// On Chrome (at least), passing pData to accessHandle.write() is
|
||||
// an error because pData is a Proxy of a Uint8Array. Calling
|
||||
// subarray() produces a real Uint8Array and that works.
|
||||
const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
|
||||
const nBytes = accessHandle.write(pData.subarray(), { at: iOffset });
|
||||
if (nBytes !== pData.byteLength) throw new Error("short write");
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {number} iSize
|
||||
* @returns {number}
|
||||
*/
|
||||
jTruncate(fileId, iSize) {
|
||||
try {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
|
||||
accessHandle.truncate(iSize);
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_TRUNCATE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {number} flags
|
||||
* @returns {number}
|
||||
*/
|
||||
jSync(fileId, flags) {
|
||||
try {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
|
||||
accessHandle.flush();
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_FSYNC;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {DataView} pSize64
|
||||
* @returns {number}
|
||||
*/
|
||||
jFileSize(fileId, pSize64) {
|
||||
try {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
|
||||
const size = accessHandle.getSize();
|
||||
pSize64.setBigInt64(0, BigInt(size), true);
|
||||
return VFS.SQLITE_OK;
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR_FSTAT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {number} lockType
|
||||
* @returns {number}
|
||||
*/
|
||||
jLock(fileId, lockType) {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
if (file.persistentFile.isRequestInProgress) {
|
||||
file.persistentFile.isLockBusy = true;
|
||||
return VFS.SQLITE_BUSY;
|
||||
}
|
||||
|
||||
file.persistentFile.isFileLocked = true;
|
||||
if (!file.persistentFile.handleLockReleaser) {
|
||||
// Start listening for notifications from other connections.
|
||||
// This is before we actually get access handles, but waiting to
|
||||
// listen until then allows a race condition where notifications
|
||||
// are missed.
|
||||
file.persistentFile.handleRequestChannel.onmessage = () => {
|
||||
this.log?.(`received notification for ${file.path}`);
|
||||
if (file.persistentFile.isFileLocked) {
|
||||
// We're still using the access handle, so mark it to be
|
||||
// released when we're done.
|
||||
file.persistentFile.isHandleRequested = true;
|
||||
} else {
|
||||
// Release the access handles immediately.
|
||||
this.#releaseAccessHandle(file);
|
||||
}
|
||||
file.persistentFile.handleRequestChannel.onmessage = null;
|
||||
};
|
||||
|
||||
this.#requestAccessHandle(file);
|
||||
this.log?.("returning SQLITE_BUSY");
|
||||
file.persistentFile.isLockBusy = true;
|
||||
return VFS.SQLITE_BUSY;
|
||||
}
|
||||
file.persistentFile.isLockBusy = false;
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {number} lockType
|
||||
* @returns {number}
|
||||
*/
|
||||
jUnlock(fileId, lockType) {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
if (lockType === VFS.SQLITE_LOCK_NONE) {
|
||||
// Don't change any state if this unlock is because xLock returned
|
||||
// SQLITE_BUSY.
|
||||
if (!file.persistentFile.isLockBusy) {
|
||||
if (file.persistentFile.isHandleRequested) {
|
||||
// Another connection wants the access handle.
|
||||
this.#releaseAccessHandle(file);
|
||||
this.isHandleRequested = false;
|
||||
}
|
||||
file.persistentFile.isFileLocked = false;
|
||||
}
|
||||
}
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fileId
|
||||
* @param {number} op
|
||||
* @param {DataView} pArg
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
jFileControl(fileId, op, pArg) {
|
||||
try {
|
||||
const file = this.mapIdToFile.get(fileId);
|
||||
switch (op) {
|
||||
case VFS.SQLITE_FCNTL_PRAGMA:
|
||||
const key = extractString(pArg, 4);
|
||||
const value = extractString(pArg, 8);
|
||||
this.log?.("xFileControl", file.path, "PRAGMA", key, value);
|
||||
switch (key.toLowerCase()) {
|
||||
case "journal_mode":
|
||||
if (value && !["off", "memory", "delete", "wal"].includes(value.toLowerCase())) {
|
||||
throw new Error('journal_mode must be "off", "memory", "delete", or "wal"');
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
this.lastError = e;
|
||||
return VFS.SQLITE_IOERR;
|
||||
}
|
||||
return VFS.SQLITE_NOTFOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} zBuf
|
||||
* @returns
|
||||
*/
|
||||
jGetLastError(zBuf) {
|
||||
if (this.lastError) {
|
||||
console.error(this.lastError);
|
||||
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
|
||||
const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
|
||||
zBuf[written] = 0;
|
||||
}
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileSystemFileHandle} fileHandle
|
||||
* @returns {Promise<PersistentFile>}
|
||||
*/
|
||||
async #createPersistentFile(fileHandle) {
|
||||
const persistentFile = new PersistentFile(fileHandle);
|
||||
const root = await navigator.storage.getDirectory();
|
||||
const relativePath = await root.resolve(fileHandle);
|
||||
const path = `/${relativePath.join("/")}`;
|
||||
persistentFile.handleRequestChannel = new BroadcastChannel(`ahp:${path}`);
|
||||
this.persistentFiles.set(path, persistentFile);
|
||||
|
||||
const f = await fileHandle.getFile();
|
||||
if (f.size) {
|
||||
this.accessiblePaths.add(path);
|
||||
}
|
||||
return persistentFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} file
|
||||
*/
|
||||
#requestAccessHandle(file) {
|
||||
console.assert(!file.persistentFile.handleLockReleaser);
|
||||
if (!file.persistentFile.isRequestInProgress) {
|
||||
file.persistentFile.isRequestInProgress = true;
|
||||
this._module.retryOps.push(
|
||||
(async () => {
|
||||
// Acquire the Web Lock.
|
||||
file.persistentFile.handleLockReleaser = await this.#acquireLock(file.persistentFile);
|
||||
|
||||
// Get access handles for the database and releated files in parallel.
|
||||
this.log?.(`creating access handles for ${file.path}`);
|
||||
await Promise.all(
|
||||
DB_RELATED_FILE_SUFFIXES.map(async (suffix) => {
|
||||
const persistentFile = this.persistentFiles.get(file.path + suffix);
|
||||
if (persistentFile) {
|
||||
persistentFile.accessHandle = await persistentFile.fileHandle.createSyncAccessHandle();
|
||||
}
|
||||
})
|
||||
);
|
||||
file.persistentFile.isRequestInProgress = false;
|
||||
})()
|
||||
);
|
||||
return this._module.retryOps.at(-1);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} file
|
||||
*/
|
||||
async #releaseAccessHandle(file) {
|
||||
DB_RELATED_FILE_SUFFIXES.forEach(async (suffix) => {
|
||||
const persistentFile = this.persistentFiles.get(file.path + suffix);
|
||||
if (persistentFile) {
|
||||
persistentFile.accessHandle?.close();
|
||||
persistentFile.accessHandle = null;
|
||||
}
|
||||
});
|
||||
this.log?.(`access handles closed for ${file.path}`);
|
||||
|
||||
file.persistentFile.handleLockReleaser?.();
|
||||
file.persistentFile.handleLockReleaser = null;
|
||||
this.log?.(`lock released for ${file.path}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PersistentFile} persistentFile
|
||||
* @returns {Promise<function>} lock releaser
|
||||
*/
|
||||
#acquireLock(persistentFile) {
|
||||
return new Promise((resolve) => {
|
||||
// Tell other connections we want the access handle.
|
||||
const lockName = persistentFile.handleRequestChannel.name;
|
||||
const notify = () => {
|
||||
this.log?.(`notifying for ${lockName}`);
|
||||
persistentFile.handleRequestChannel.postMessage(null);
|
||||
};
|
||||
const notifyId = setInterval(notify, LOCK_NOTIFY_INTERVAL);
|
||||
setTimeout(notify);
|
||||
|
||||
this.log?.(`lock requested: ${lockName}`);
|
||||
navigator.locks.request(lockName, (lock) => {
|
||||
// We have the lock. Stop asking other connections for it.
|
||||
this.log?.(`lock acquired: ${lockName}`, lock);
|
||||
clearInterval(notifyId);
|
||||
return new Promise(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function extractString(dataView, offset) {
|
||||
const p = dataView.getUint32(offset, true);
|
||||
if (p) {
|
||||
const chars = new Uint8Array(dataView.buffer, p);
|
||||
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
222
web/core/local-db/worker/wa-sqlite/src/VFS.js
Normal file
222
web/core/local-db/worker/wa-sqlite/src/VFS.js
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
|
||||
import * as VFS from './sqlite-constants.js';
|
||||
export * from './sqlite-constants.js';
|
||||
|
||||
const DEFAULT_SECTOR_SIZE = 512;
|
||||
|
||||
// Base class for a VFS.
|
||||
export class Base {
|
||||
name;
|
||||
mxPathname = 64;
|
||||
_module;
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {object} module
|
||||
*/
|
||||
constructor(name, module) {
|
||||
this.name = name;
|
||||
this._module = module;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void|Promise<void>}
|
||||
*/
|
||||
close() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean|Promise<boolean>}
|
||||
*/
|
||||
isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload in subclasses to indicate which methods are asynchronous.
|
||||
* @param {string} methodName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAsyncMethod(methodName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} pFile
|
||||
* @param {number} flags
|
||||
* @param {number} pOutFlags
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xOpen(pVfs, zName, pFile, flags, pOutFlags) {
|
||||
return VFS.SQLITE_CANTOPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} syncDir
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xDelete(pVfs, zName, syncDir) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} flags
|
||||
* @param {number} pResOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xAccess(pVfs, zName, flags, pResOut) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} zName
|
||||
* @param {number} nOut
|
||||
* @param {number} zOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xFullPathname(pVfs, zName, nOut, zOut) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pVfs
|
||||
* @param {number} nBuf
|
||||
* @param {number} zBuf
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xGetLastError(pVfs, nBuf, zBuf) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xClose(pFile) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} pData
|
||||
* @param {number} iAmt
|
||||
* @param {number} iOffsetLo
|
||||
* @param {number} iOffsetHi
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xRead(pFile, pData, iAmt, iOffsetLo, iOffsetHi) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} pData
|
||||
* @param {number} iAmt
|
||||
* @param {number} iOffsetLo
|
||||
* @param {number} iOffsetHi
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xWrite(pFile, pData, iAmt, iOffsetLo, iOffsetHi) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} sizeLo
|
||||
* @param {number} sizeHi
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xTruncate(pFile, sizeLo, sizeHi) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} flags
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xSync(pFile, flags) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} pFile
|
||||
* @param {number} pSize
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xFileSize(pFile, pSize) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} lockType
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xLock(pFile, lockType) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} lockType
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xUnlock(pFile, lockType) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} pResOut
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xCheckReservedLock(pFile, pResOut) {
|
||||
return VFS.SQLITE_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @param {number} op
|
||||
* @param {number} pArg
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xFileControl(pFile, op, pArg) {
|
||||
return VFS.SQLITE_NOTFOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xSectorSize(pFile) {
|
||||
return DEFAULT_SECTOR_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pFile
|
||||
* @returns {number|Promise<number>}
|
||||
*/
|
||||
xDeviceCharacteristics(pFile) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const FILE_TYPE_MASK = [
|
||||
VFS.SQLITE_OPEN_MAIN_DB,
|
||||
VFS.SQLITE_OPEN_MAIN_JOURNAL,
|
||||
VFS.SQLITE_OPEN_TEMP_DB,
|
||||
VFS.SQLITE_OPEN_TEMP_JOURNAL,
|
||||
VFS.SQLITE_OPEN_TRANSIENT_DB,
|
||||
VFS.SQLITE_OPEN_SUBJOURNAL,
|
||||
VFS.SQLITE_OPEN_SUPER_JOURNAL,
|
||||
VFS.SQLITE_OPEN_WAL
|
||||
].reduce((mask, element) => mask | element);
|
||||
899
web/core/local-db/worker/wa-sqlite/src/sqlite-api.js
Normal file
899
web/core/local-db/worker/wa-sqlite/src/sqlite-api.js
Normal file
|
|
@ -0,0 +1,899 @@
|
|||
// Copyright 2021 Roy T. Hashimoto. All Rights Reserved.
|
||||
|
||||
import * as SQLite from "./sqlite-constants.js";
|
||||
export * from "./sqlite-constants.js";
|
||||
|
||||
const MAX_INT64 = 0x7fffffffffffffffn;
|
||||
const MIN_INT64 = -0x8000000000000000n;
|
||||
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
|
||||
export class SQLiteError extends Error {
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const async = true;
|
||||
|
||||
/**
|
||||
* Builds a Javascript API from the Emscripten module. This API is still
|
||||
* low-level and closely corresponds to the C API exported by the module,
|
||||
* but differs in some specifics like throwing exceptions on errors.
|
||||
* @param {*} Module SQLite Emscripten module
|
||||
* @returns {SQLiteAPI}
|
||||
*/
|
||||
export function Factory(Module) {
|
||||
/** @type {SQLiteAPI} */ const sqlite3 = {};
|
||||
|
||||
Module.retryOps = [];
|
||||
const sqliteFreeAddress = Module._getSqliteFree();
|
||||
|
||||
// Allocate some space for 32-bit returned values.
|
||||
const tmp = Module._malloc(8);
|
||||
const tmpPtr = [tmp, tmp + 4];
|
||||
|
||||
// Convert a JS string to a C string. sqlite3_malloc is used to allocate
|
||||
// memory (use sqlite3_free to deallocate).
|
||||
function createUTF8(s) {
|
||||
if (typeof s !== "string") return 0;
|
||||
const utf8 = new TextEncoder().encode(s);
|
||||
const zts = Module._sqlite3_malloc(utf8.byteLength + 1);
|
||||
Module.HEAPU8.set(utf8, zts);
|
||||
Module.HEAPU8[zts + utf8.byteLength] = 0;
|
||||
return zts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate 32-bit numbers into a 64-bit (signed) BigInt.
|
||||
* @param {number} lo32
|
||||
* @param {number} hi32
|
||||
* @returns {bigint}
|
||||
*/
|
||||
function cvt32x2ToBigInt(lo32, hi32) {
|
||||
return (BigInt(hi32) << 32n) | (BigInt(lo32) & 0xffffffffn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate 32-bit numbers and return as number or BigInt, depending
|
||||
* on the value.
|
||||
* @param {number} lo32
|
||||
* @param {number} hi32
|
||||
* @returns {number|bigint}
|
||||
*/
|
||||
const cvt32x2AsSafe = (function () {
|
||||
const hiMax = BigInt(Number.MAX_SAFE_INTEGER) >> 32n;
|
||||
const hiMin = BigInt(Number.MIN_SAFE_INTEGER) >> 32n;
|
||||
|
||||
return function (lo32, hi32) {
|
||||
if (hi32 > hiMax || hi32 < hiMin) {
|
||||
// Can't be expressed as a Number so use BigInt.
|
||||
return cvt32x2ToBigInt(lo32, hi32);
|
||||
} else {
|
||||
// Combine the upper and lower 32-bit numbers. The complication is
|
||||
// that lo32 is a signed integer which makes manipulating its bits
|
||||
// a little tricky - the sign bit gets handled separately.
|
||||
return hi32 * 0x100000000 + (lo32 & 0x7fffffff) - (lo32 & 0x80000000);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
const databases = new Set();
|
||||
function verifyDatabase(db) {
|
||||
if (!databases.has(db)) {
|
||||
throw new SQLiteError("not a database", SQLite.SQLITE_MISUSE);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStmtToDB = new Map();
|
||||
function verifyStatement(stmt) {
|
||||
if (!mapStmtToDB.has(stmt)) {
|
||||
throw new SQLiteError("not a statement", SQLite.SQLITE_MISUSE);
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3.bind_collection = function (stmt, bindings) {
|
||||
verifyStatement(stmt);
|
||||
const isArray = Array.isArray(bindings);
|
||||
const nBindings = sqlite3.bind_parameter_count(stmt);
|
||||
for (let i = 1; i <= nBindings; ++i) {
|
||||
const key = isArray ? i - 1 : sqlite3.bind_parameter_name(stmt, i);
|
||||
const value = bindings[key];
|
||||
if (value !== undefined) {
|
||||
sqlite3.bind(stmt, i, value);
|
||||
}
|
||||
}
|
||||
return SQLite.SQLITE_OK;
|
||||
};
|
||||
|
||||
sqlite3.bind = function (stmt, i, value) {
|
||||
verifyStatement(stmt);
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
if (value === (value | 0)) {
|
||||
return sqlite3.bind_int(stmt, i, value);
|
||||
} else {
|
||||
return sqlite3.bind_double(stmt, i, value);
|
||||
}
|
||||
case "string":
|
||||
return sqlite3.bind_text(stmt, i, value);
|
||||
default:
|
||||
if (value instanceof Uint8Array || Array.isArray(value)) {
|
||||
return sqlite3.bind_blob(stmt, i, value);
|
||||
} else if (value === null) {
|
||||
return sqlite3.bind_null(stmt, i);
|
||||
} else if (typeof value === "bigint") {
|
||||
return sqlite3.bind_int64(stmt, i, value);
|
||||
} else if (value === undefined) {
|
||||
// Existing binding (or NULL) will be used.
|
||||
return SQLite.SQLITE_NOTICE;
|
||||
} else {
|
||||
console.warn("unknown binding converted to null", value);
|
||||
return sqlite3.bind_null(stmt, i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sqlite3.bind_blob = (function () {
|
||||
const fname = "sqlite3_bind_blob";
|
||||
const f = Module.cwrap(fname, ...decl("nnnnn:n"));
|
||||
return function (stmt, i, value) {
|
||||
verifyStatement(stmt);
|
||||
// @ts-ignore
|
||||
const byteLength = value.byteLength ?? value.length;
|
||||
const ptr = Module._sqlite3_malloc(byteLength);
|
||||
Module.HEAPU8.subarray(ptr).set(value);
|
||||
const result = f(stmt, i, ptr, byteLength, sqliteFreeAddress);
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.bind_parameter_count = (function () {
|
||||
const fname = "sqlite3_bind_parameter_count";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (stmt) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.bind_double = (function () {
|
||||
const fname = "sqlite3_bind_double";
|
||||
const f = Module.cwrap(fname, ...decl("nnn:n"));
|
||||
return function (stmt, i, value) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, i, value);
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.bind_int = (function () {
|
||||
const fname = "sqlite3_bind_int";
|
||||
const f = Module.cwrap(fname, ...decl("nnn:n"));
|
||||
return function (stmt, i, value) {
|
||||
verifyStatement(stmt);
|
||||
if (value > 0x7fffffff || value < -0x80000000) return SQLite.SQLITE_RANGE;
|
||||
|
||||
const result = f(stmt, i, value);
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.bind_int64 = (function () {
|
||||
const fname = "sqlite3_bind_int64";
|
||||
const f = Module.cwrap(fname, ...decl("nnnn:n"));
|
||||
return function (stmt, i, value) {
|
||||
verifyStatement(stmt);
|
||||
if (value > MAX_INT64 || value < MIN_INT64) return SQLite.SQLITE_RANGE;
|
||||
|
||||
const lo32 = value & 0xffffffffn;
|
||||
const hi32 = value >> 32n;
|
||||
const result = f(stmt, i, Number(lo32), Number(hi32));
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.bind_null = (function () {
|
||||
const fname = "sqlite3_bind_null";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (stmt, i) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, i);
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.bind_parameter_name = (function () {
|
||||
const fname = "sqlite3_bind_parameter_name";
|
||||
const f = Module.cwrap(fname, ...decl("n:s"));
|
||||
return function (stmt, i) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, i);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.bind_text = (function () {
|
||||
const fname = "sqlite3_bind_text";
|
||||
const f = Module.cwrap(fname, ...decl("nnnnn:n"));
|
||||
return function (stmt, i, value) {
|
||||
verifyStatement(stmt);
|
||||
const ptr = createUTF8(value);
|
||||
const result = f(stmt, i, ptr, -1, sqliteFreeAddress);
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.changes = (function () {
|
||||
const fname = "sqlite3_changes";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (db) {
|
||||
verifyDatabase(db);
|
||||
const result = f(db);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.clear_bindings = (function () {
|
||||
const fname = "sqlite3_clear_bindings";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (stmt) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt);
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.close = (function () {
|
||||
const fname = "sqlite3_close";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"), { async });
|
||||
return async function (db) {
|
||||
verifyDatabase(db);
|
||||
const result = await f(db);
|
||||
databases.delete(db);
|
||||
return check(fname, result, db);
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column = function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const type = sqlite3.column_type(stmt, iCol);
|
||||
switch (type) {
|
||||
case SQLite.SQLITE_BLOB:
|
||||
return sqlite3.column_blob(stmt, iCol);
|
||||
case SQLite.SQLITE_FLOAT:
|
||||
return sqlite3.column_double(stmt, iCol);
|
||||
case SQLite.SQLITE_INTEGER:
|
||||
const lo32 = sqlite3.column_int(stmt, iCol);
|
||||
const hi32 = Module.getTempRet0();
|
||||
return cvt32x2AsSafe(lo32, hi32);
|
||||
case SQLite.SQLITE_NULL:
|
||||
return null;
|
||||
case SQLite.SQLITE_TEXT:
|
||||
return sqlite3.column_text(stmt, iCol);
|
||||
default:
|
||||
throw new SQLiteError("unknown type", type);
|
||||
}
|
||||
};
|
||||
|
||||
sqlite3.column_blob = (function () {
|
||||
const fname = "sqlite3_column_blob";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const nBytes = sqlite3.column_bytes(stmt, iCol);
|
||||
const address = f(stmt, iCol);
|
||||
const result = Module.HEAPU8.subarray(address, address + nBytes);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_bytes = (function () {
|
||||
const fname = "sqlite3_column_bytes";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, iCol);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_count = (function () {
|
||||
const fname = "sqlite3_column_count";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (stmt) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_double = (function () {
|
||||
const fname = "sqlite3_column_double";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, iCol);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_int = (function () {
|
||||
// Retrieve int64 but use only the lower 32 bits. The upper 32-bits are
|
||||
// accessible with Module.getTempRet0().
|
||||
const fname = "sqlite3_column_int64";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, iCol);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_int64 = (function () {
|
||||
const fname = "sqlite3_column_int64";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const lo32 = f(stmt, iCol);
|
||||
const hi32 = Module.getTempRet0();
|
||||
const result = cvt32x2ToBigInt(lo32, hi32);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_name = (function () {
|
||||
const fname = "sqlite3_column_name";
|
||||
const f = Module.cwrap(fname, ...decl("nn:s"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, iCol);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_names = function (stmt) {
|
||||
const columns = [];
|
||||
const nColumns = sqlite3.column_count(stmt);
|
||||
for (let i = 0; i < nColumns; ++i) {
|
||||
columns.push(sqlite3.column_name(stmt, i));
|
||||
}
|
||||
return columns;
|
||||
};
|
||||
|
||||
sqlite3.column_text = (function () {
|
||||
const fname = "sqlite3_column_text";
|
||||
const f = Module.cwrap(fname, ...decl("nn:s"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, iCol);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.column_type = (function () {
|
||||
const fname = "sqlite3_column_type";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (stmt, iCol) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt, iCol);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.create_function = function (db, zFunctionName, nArg, eTextRep, pApp, xFunc, xStep, xFinal) {
|
||||
verifyDatabase(db);
|
||||
|
||||
// Convert SQLite callback arguments to JavaScript-friendly arguments.
|
||||
function adapt(f) {
|
||||
return f instanceof AsyncFunction
|
||||
? async (ctx, n, values) => f(ctx, Module.HEAP32.subarray(values / 4, values / 4 + n))
|
||||
: (ctx, n, values) => f(ctx, Module.HEAP32.subarray(values / 4, values / 4 + n));
|
||||
}
|
||||
|
||||
const result = Module.create_function(
|
||||
db,
|
||||
zFunctionName,
|
||||
nArg,
|
||||
eTextRep,
|
||||
pApp,
|
||||
xFunc && adapt(xFunc),
|
||||
xStep && adapt(xStep),
|
||||
xFinal
|
||||
);
|
||||
return check("sqlite3_create_function", result, db);
|
||||
};
|
||||
|
||||
sqlite3.data_count = (function () {
|
||||
const fname = "sqlite3_data_count";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (stmt) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.exec = async function (db, sql, callback) {
|
||||
for await (const stmt of sqlite3.statements(db, sql)) {
|
||||
let columns;
|
||||
while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
|
||||
if (callback) {
|
||||
columns = columns ?? sqlite3.column_names(stmt);
|
||||
const row = sqlite3.row(stmt);
|
||||
await callback(row, columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
return SQLite.SQLITE_OK;
|
||||
};
|
||||
|
||||
sqlite3.finalize = (function () {
|
||||
const fname = "sqlite3_finalize";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"), { async });
|
||||
return async function (stmt) {
|
||||
const result = await f(stmt);
|
||||
mapStmtToDB.delete(stmt);
|
||||
|
||||
// Don't throw on error here. Typically the error has already been
|
||||
// thrown and finalize() is part of the cleanup.
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.get_autocommit = (function () {
|
||||
const fname = "sqlite3_get_autocommit";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (db) {
|
||||
const result = f(db);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.libversion = (function () {
|
||||
const fname = "sqlite3_libversion";
|
||||
const f = Module.cwrap(fname, ...decl(":s"));
|
||||
return function () {
|
||||
const result = f();
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.libversion_number = (function () {
|
||||
const fname = "sqlite3_libversion_number";
|
||||
const f = Module.cwrap(fname, ...decl(":n"));
|
||||
return function () {
|
||||
const result = f();
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.limit = (function () {
|
||||
const fname = "sqlite3_limit";
|
||||
const f = Module.cwrap(fname, ...decl("nnn:n"));
|
||||
return function (db, id, newVal) {
|
||||
const result = f(db, id, newVal);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.open_v2 = (function () {
|
||||
const fname = "sqlite3_open_v2";
|
||||
const f = Module.cwrap(fname, ...decl("snnn:n"), { async });
|
||||
return async function (zFilename, flags, zVfs) {
|
||||
flags = flags || SQLite.SQLITE_OPEN_CREATE | SQLite.SQLITE_OPEN_READWRITE;
|
||||
zVfs = createUTF8(zVfs);
|
||||
try {
|
||||
// Allow retry operations.
|
||||
const rc = await retry(() => f(zFilename, tmpPtr[0], flags, zVfs));
|
||||
|
||||
const db = Module.getValue(tmpPtr[0], "*");
|
||||
databases.add(db);
|
||||
|
||||
Module.ccall("RegisterExtensionFunctions", "void", ["number"], [db]);
|
||||
check(fname, rc);
|
||||
return db;
|
||||
} finally {
|
||||
Module._sqlite3_free(zVfs);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.progress_handler = function (db, nProgressOps, handler, userData) {
|
||||
verifyDatabase(db);
|
||||
Module.progress_handler(db, nProgressOps, handler, userData);
|
||||
};
|
||||
|
||||
sqlite3.reset = (function () {
|
||||
const fname = "sqlite3_reset";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"), { async });
|
||||
return async function (stmt) {
|
||||
verifyStatement(stmt);
|
||||
const result = await f(stmt);
|
||||
return check(fname, result, mapStmtToDB.get(stmt));
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.result = function (context, value) {
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
if (value === (value | 0)) {
|
||||
sqlite3.result_int(context, value);
|
||||
} else {
|
||||
sqlite3.result_double(context, value);
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
sqlite3.result_text(context, value);
|
||||
break;
|
||||
default:
|
||||
if (value instanceof Uint8Array || Array.isArray(value)) {
|
||||
sqlite3.result_blob(context, value);
|
||||
} else if (value === null) {
|
||||
sqlite3.result_null(context);
|
||||
} else if (typeof value === "bigint") {
|
||||
return sqlite3.result_int64(context, value);
|
||||
} else {
|
||||
console.warn("unknown result converted to null", value);
|
||||
sqlite3.result_null(context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
sqlite3.result_blob = (function () {
|
||||
const fname = "sqlite3_result_blob";
|
||||
const f = Module.cwrap(fname, ...decl("nnnn:n"));
|
||||
return function (context, value) {
|
||||
// @ts-ignore
|
||||
const byteLength = value.byteLength ?? value.length;
|
||||
const ptr = Module._sqlite3_malloc(byteLength);
|
||||
Module.HEAPU8.subarray(ptr).set(value);
|
||||
f(context, ptr, byteLength, sqliteFreeAddress); // void return
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.result_double = (function () {
|
||||
const fname = "sqlite3_result_double";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (context, value) {
|
||||
f(context, value); // void return
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.result_int = (function () {
|
||||
const fname = "sqlite3_result_int";
|
||||
const f = Module.cwrap(fname, ...decl("nn:n"));
|
||||
return function (context, value) {
|
||||
f(context, value); // void return
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.result_int64 = (function () {
|
||||
const fname = "sqlite3_result_int64";
|
||||
const f = Module.cwrap(fname, ...decl("nnn:n"));
|
||||
return function (context, value) {
|
||||
if (value > MAX_INT64 || value < MIN_INT64) return SQLite.SQLITE_RANGE;
|
||||
|
||||
const lo32 = value & 0xffffffffn;
|
||||
const hi32 = value >> 32n;
|
||||
f(context, Number(lo32), Number(hi32)); // void return
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.result_null = (function () {
|
||||
const fname = "sqlite3_result_null";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (context) {
|
||||
f(context); // void return
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.result_text = (function () {
|
||||
const fname = "sqlite3_result_text";
|
||||
const f = Module.cwrap(fname, ...decl("nnnn:n"));
|
||||
return function (context, value) {
|
||||
const ptr = createUTF8(value);
|
||||
f(context, ptr, -1, sqliteFreeAddress); // void return
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.row = function (stmt) {
|
||||
const row = [];
|
||||
const nColumns = sqlite3.data_count(stmt);
|
||||
for (let i = 0; i < nColumns; ++i) {
|
||||
const value = sqlite3.column(stmt, i);
|
||||
|
||||
// Copy blob if aliasing volatile WebAssembly memory. This avoids an
|
||||
// unnecessary copy if users monkey patch column_blob to copy.
|
||||
// @ts-ignore
|
||||
row.push(value?.buffer === Module.HEAPU8.buffer ? value.slice() : value);
|
||||
}
|
||||
return row;
|
||||
};
|
||||
|
||||
sqlite3.set_authorizer = function (db, xAuth, pApp) {
|
||||
verifyDatabase(db);
|
||||
|
||||
// Convert SQLite callback arguments to JavaScript-friendly arguments.
|
||||
function cvtArgs(_, iAction, p3, p4, p5, p6) {
|
||||
return [
|
||||
_,
|
||||
iAction,
|
||||
Module.UTF8ToString(p3),
|
||||
Module.UTF8ToString(p4),
|
||||
Module.UTF8ToString(p5),
|
||||
Module.UTF8ToString(p6),
|
||||
];
|
||||
}
|
||||
function adapt(f) {
|
||||
return f instanceof AsyncFunction
|
||||
? async (_, iAction, p3, p4, p5, p6) => f(...cvtArgs(_, iAction, p3, p4, p5, p6))
|
||||
: (_, iAction, p3, p4, p5, p6) => f(...cvtArgs(_, iAction, p3, p4, p5, p6));
|
||||
}
|
||||
|
||||
const result = Module.set_authorizer(db, adapt(xAuth), pApp);
|
||||
return check("sqlite3_set_authorizer", result, db);
|
||||
};
|
||||
|
||||
sqlite3.sql = (function () {
|
||||
const fname = "sqlite3_sql";
|
||||
const f = Module.cwrap(fname, ...decl("n:s"));
|
||||
return function (stmt) {
|
||||
verifyStatement(stmt);
|
||||
const result = f(stmt);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.statements = function (db, sql, options = {}) {
|
||||
const prepare = Module.cwrap(
|
||||
"sqlite3_prepare_v3",
|
||||
"number",
|
||||
["number", "number", "number", "number", "number", "number"],
|
||||
{ async: true }
|
||||
);
|
||||
|
||||
return (async function* () {
|
||||
const onFinally = [];
|
||||
try {
|
||||
// Encode SQL string to UTF-8.
|
||||
const utf8 = new TextEncoder().encode(sql);
|
||||
|
||||
// Copy encoded string to WebAssembly memory. The SQLite docs say
|
||||
// zero-termination is a minor optimization so add room for that.
|
||||
// Also add space for the statement handle and SQL tail pointer.
|
||||
const allocSize = utf8.byteLength - (utf8.byteLength % 4) + 12;
|
||||
const pzHead = Module._sqlite3_malloc(allocSize);
|
||||
const pzEnd = pzHead + utf8.byteLength + 1;
|
||||
onFinally.push(() => Module._sqlite3_free(pzHead));
|
||||
Module.HEAPU8.set(utf8, pzHead);
|
||||
Module.HEAPU8[pzEnd - 1] = 0;
|
||||
|
||||
// Use extra space for the statement handle and SQL tail pointer.
|
||||
const pStmt = pzHead + allocSize - 8;
|
||||
const pzTail = pzHead + allocSize - 4;
|
||||
|
||||
// Ensure that statement handles are not leaked.
|
||||
let stmt;
|
||||
function maybeFinalize() {
|
||||
if (stmt && !options.unscoped) {
|
||||
sqlite3.finalize(stmt);
|
||||
}
|
||||
stmt = 0;
|
||||
}
|
||||
onFinally.push(maybeFinalize);
|
||||
|
||||
// Loop over statements.
|
||||
Module.setValue(pzTail, pzHead, "*");
|
||||
do {
|
||||
// Reclaim resources for the previous iteration.
|
||||
maybeFinalize();
|
||||
|
||||
// Call sqlite3_prepare_v3() for the next statement.
|
||||
// Allow retry operations.
|
||||
const zTail = Module.getValue(pzTail, "*");
|
||||
const rc = await retry(() => {
|
||||
return prepare(db, zTail, pzEnd - pzTail, options.flags || 0, pStmt, pzTail);
|
||||
});
|
||||
|
||||
if (rc !== SQLite.SQLITE_OK) {
|
||||
check("sqlite3_prepare_v3", rc, db);
|
||||
}
|
||||
|
||||
stmt = Module.getValue(pStmt, "*");
|
||||
if (stmt) {
|
||||
mapStmtToDB.set(stmt, db);
|
||||
yield stmt;
|
||||
}
|
||||
} while (stmt);
|
||||
} finally {
|
||||
while (onFinally.length) {
|
||||
onFinally.pop()();
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
sqlite3.step = (function () {
|
||||
const fname = "sqlite3_step";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"), { async });
|
||||
return async function (stmt) {
|
||||
verifyStatement(stmt);
|
||||
|
||||
// Allow retry operations.
|
||||
const rc = await retry(() => f(stmt));
|
||||
|
||||
return check(fname, rc, mapStmtToDB.get(stmt), [SQLite.SQLITE_ROW, SQLite.SQLITE_DONE]);
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.update_hook = function (db, xUpdateHook) {
|
||||
verifyDatabase(db);
|
||||
|
||||
// Convert SQLite callback arguments to JavaScript-friendly arguments.
|
||||
function cvtArgs(iUpdateType, dbName, tblName, lo32, hi32) {
|
||||
return [iUpdateType, Module.UTF8ToString(dbName), Module.UTF8ToString(tblName), cvt32x2ToBigInt(lo32, hi32)];
|
||||
}
|
||||
function adapt(f) {
|
||||
return f instanceof AsyncFunction
|
||||
? async (iUpdateType, dbName, tblName, lo32, hi32) => f(...cvtArgs(iUpdateType, dbName, tblName, lo32, hi32))
|
||||
: (iUpdateType, dbName, tblName, lo32, hi32) => f(...cvtArgs(iUpdateType, dbName, tblName, lo32, hi32));
|
||||
}
|
||||
|
||||
Module.update_hook(db, adapt(xUpdateHook));
|
||||
};
|
||||
|
||||
sqlite3.value = function (pValue) {
|
||||
const type = sqlite3.value_type(pValue);
|
||||
switch (type) {
|
||||
case SQLite.SQLITE_BLOB:
|
||||
return sqlite3.value_blob(pValue);
|
||||
case SQLite.SQLITE_FLOAT:
|
||||
return sqlite3.value_double(pValue);
|
||||
case SQLite.SQLITE_INTEGER:
|
||||
const lo32 = sqlite3.value_int(pValue);
|
||||
const hi32 = Module.getTempRet0();
|
||||
return cvt32x2AsSafe(lo32, hi32);
|
||||
case SQLite.SQLITE_NULL:
|
||||
return null;
|
||||
case SQLite.SQLITE_TEXT:
|
||||
return sqlite3.value_text(pValue);
|
||||
default:
|
||||
throw new SQLiteError("unknown type", type);
|
||||
}
|
||||
};
|
||||
|
||||
sqlite3.value_blob = (function () {
|
||||
const fname = "sqlite3_value_blob";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (pValue) {
|
||||
const nBytes = sqlite3.value_bytes(pValue);
|
||||
const address = f(pValue);
|
||||
const result = Module.HEAPU8.subarray(address, address + nBytes);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.value_bytes = (function () {
|
||||
const fname = "sqlite3_value_bytes";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (pValue) {
|
||||
const result = f(pValue);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.value_double = (function () {
|
||||
const fname = "sqlite3_value_double";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (pValue) {
|
||||
const result = f(pValue);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.value_int = (function () {
|
||||
const fname = "sqlite3_value_int64";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (pValue) {
|
||||
const result = f(pValue);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.value_int64 = (function () {
|
||||
const fname = "sqlite3_value_int64";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (pValue) {
|
||||
const lo32 = f(pValue);
|
||||
const hi32 = Module.getTempRet0();
|
||||
const result = cvt32x2ToBigInt(lo32, hi32);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.value_text = (function () {
|
||||
const fname = "sqlite3_value_text";
|
||||
const f = Module.cwrap(fname, ...decl("n:s"));
|
||||
return function (pValue) {
|
||||
const result = f(pValue);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.value_type = (function () {
|
||||
const fname = "sqlite3_value_type";
|
||||
const f = Module.cwrap(fname, ...decl("n:n"));
|
||||
return function (pValue) {
|
||||
const result = f(pValue);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
sqlite3.vfs_register = function (vfs, makeDefault) {
|
||||
const result = Module.vfs_register(vfs, makeDefault);
|
||||
return check("sqlite3_vfs_register", result);
|
||||
};
|
||||
|
||||
function check(fname, result, db = null, allowed = [SQLite.SQLITE_OK]) {
|
||||
if (allowed.includes(result)) return result;
|
||||
const message = db ? Module.ccall("sqlite3_errmsg", "string", ["number"], [db]) : fname;
|
||||
throw new SQLiteError(message, result);
|
||||
}
|
||||
|
||||
// This function is used to automatically retry failed calls that
|
||||
// have pending retry operations that should allow the retry to
|
||||
// succeed.
|
||||
async function retry(f) {
|
||||
let rc;
|
||||
do {
|
||||
// Wait for all pending retry operations to complete. This is
|
||||
// normally empty on the first loop iteration.
|
||||
if (Module.retryOps.length) {
|
||||
await Promise.all(Module.retryOps);
|
||||
Module.retryOps = [];
|
||||
}
|
||||
|
||||
rc = await f();
|
||||
|
||||
// Retry on failure with new pending retry operations.
|
||||
} while (rc && Module.retryOps.length);
|
||||
return rc;
|
||||
}
|
||||
|
||||
return sqlite3;
|
||||
}
|
||||
|
||||
// Helper function to use a more compact signature specification.
|
||||
function decl(s) {
|
||||
const result = [];
|
||||
const m = s.match(/([ns@]*):([nsv@])/);
|
||||
switch (m[2]) {
|
||||
case "n":
|
||||
result.push("number");
|
||||
break;
|
||||
case "s":
|
||||
result.push("string");
|
||||
break;
|
||||
case "v":
|
||||
result.push(null);
|
||||
break;
|
||||
}
|
||||
|
||||
const args = [];
|
||||
for (let c of m[1]) {
|
||||
switch (c) {
|
||||
case "n":
|
||||
args.push("number");
|
||||
break;
|
||||
case "s":
|
||||
args.push("string");
|
||||
break;
|
||||
}
|
||||
}
|
||||
result.push(args);
|
||||
return result;
|
||||
}
|
||||
275
web/core/local-db/worker/wa-sqlite/src/sqlite-constants.js
Normal file
275
web/core/local-db/worker/wa-sqlite/src/sqlite-constants.js
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
// Primary result codes.
|
||||
// https://www.sqlite.org/rescode.html
|
||||
export const SQLITE_OK = 0;
|
||||
export const SQLITE_ERROR = 1;
|
||||
export const SQLITE_INTERNAL = 2;
|
||||
export const SQLITE_PERM = 3;
|
||||
export const SQLITE_ABORT = 4;
|
||||
export const SQLITE_BUSY = 5;
|
||||
export const SQLITE_LOCKED = 6;
|
||||
export const SQLITE_NOMEM = 7;
|
||||
export const SQLITE_READONLY = 8;
|
||||
export const SQLITE_INTERRUPT = 9;
|
||||
export const SQLITE_IOERR = 10;
|
||||
export const SQLITE_CORRUPT = 11;
|
||||
export const SQLITE_NOTFOUND = 12;
|
||||
export const SQLITE_FULL = 13;
|
||||
export const SQLITE_CANTOPEN = 14;
|
||||
export const SQLITE_PROTOCOL = 15;
|
||||
export const SQLITE_EMPTY = 16;
|
||||
export const SQLITE_SCHEMA = 17;
|
||||
export const SQLITE_TOOBIG = 18;
|
||||
export const SQLITE_CONSTRAINT = 19;
|
||||
export const SQLITE_MISMATCH = 20;
|
||||
export const SQLITE_MISUSE = 21;
|
||||
export const SQLITE_NOLFS = 22;
|
||||
export const SQLITE_AUTH = 23;
|
||||
export const SQLITE_FORMAT = 24;
|
||||
export const SQLITE_RANGE = 25;
|
||||
export const SQLITE_NOTADB = 26;
|
||||
export const SQLITE_NOTICE = 27;
|
||||
export const SQLITE_WARNING = 28;
|
||||
export const SQLITE_ROW = 100;
|
||||
export const SQLITE_DONE = 101;
|
||||
|
||||
// Extended error codes.
|
||||
export const SQLITE_IOERR_ACCESS = 3338;
|
||||
export const SQLITE_IOERR_CHECKRESERVEDLOCK = 3594;
|
||||
export const SQLITE_IOERR_CLOSE = 4106;
|
||||
export const SQLITE_IOERR_DATA = 8202;
|
||||
export const SQLITE_IOERR_DELETE = 2570;
|
||||
export const SQLITE_IOERR_DELETE_NOENT = 5898;
|
||||
export const SQLITE_IOERR_DIR_FSYNC = 1290;
|
||||
export const SQLITE_IOERR_FSTAT = 1802;
|
||||
export const SQLITE_IOERR_FSYNC = 1034;
|
||||
export const SQLITE_IOERR_GETTEMPPATH = 6410;
|
||||
export const SQLITE_IOERR_LOCK = 3850;
|
||||
export const SQLITE_IOERR_NOMEM = 3082;
|
||||
export const SQLITE_IOERR_READ = 266;
|
||||
export const SQLITE_IOERR_RDLOCK = 2314;
|
||||
export const SQLITE_IOERR_SEEK = 5642;
|
||||
export const SQLITE_IOERR_SHORT_READ = 522;
|
||||
export const SQLITE_IOERR_TRUNCATE = 1546;
|
||||
export const SQLITE_IOERR_UNLOCK = 2058;
|
||||
export const SQLITE_IOERR_VNODE = 6922;
|
||||
export const SQLITE_IOERR_WRITE = 778;
|
||||
export const SQLITE_IOERR_BEGIN_ATOMIC = 7434;
|
||||
export const SQLITE_IOERR_COMMIT_ATOMIC = 7690;
|
||||
export const SQLITE_IOERR_ROLLBACK_ATOMIC = 7946;
|
||||
|
||||
// Other extended result codes.
|
||||
export const SQLITE_CONSTRAINT_CHECK = 275;
|
||||
export const SQLITE_CONSTRAINT_COMMITHOOK = 531;
|
||||
export const SQLITE_CONSTRAINT_FOREIGNKEY = 787;
|
||||
export const SQLITE_CONSTRAINT_FUNCTION = 1043;
|
||||
export const SQLITE_CONSTRAINT_NOTNULL = 1299;
|
||||
export const SQLITE_CONSTRAINT_PINNED = 2835;
|
||||
export const SQLITE_CONSTRAINT_PRIMARYKEY = 1555;
|
||||
export const SQLITE_CONSTRAINT_ROWID = 2579;
|
||||
export const SQLITE_CONSTRAINT_TRIGGER = 1811;
|
||||
export const SQLITE_CONSTRAINT_UNIQUE = 2067;
|
||||
export const SQLITE_CONSTRAINT_VTAB = 2323;
|
||||
|
||||
// Open flags.
|
||||
// https://www.sqlite.org/c3ref/c_open_autoproxy.html
|
||||
export const SQLITE_OPEN_READONLY = 0x00000001;
|
||||
export const SQLITE_OPEN_READWRITE = 0x00000002;
|
||||
export const SQLITE_OPEN_CREATE = 0x00000004;
|
||||
export const SQLITE_OPEN_DELETEONCLOSE = 0x00000008;
|
||||
export const SQLITE_OPEN_EXCLUSIVE = 0x00000010;
|
||||
export const SQLITE_OPEN_AUTOPROXY = 0x00000020;
|
||||
export const SQLITE_OPEN_URI = 0x00000040;
|
||||
export const SQLITE_OPEN_MEMORY = 0x00000080;
|
||||
export const SQLITE_OPEN_MAIN_DB = 0x00000100;
|
||||
export const SQLITE_OPEN_TEMP_DB = 0x00000200;
|
||||
export const SQLITE_OPEN_TRANSIENT_DB = 0x00000400;
|
||||
export const SQLITE_OPEN_MAIN_JOURNAL = 0x00000800;
|
||||
export const SQLITE_OPEN_TEMP_JOURNAL = 0x00001000;
|
||||
export const SQLITE_OPEN_SUBJOURNAL = 0x00002000;
|
||||
export const SQLITE_OPEN_SUPER_JOURNAL = 0x00004000;
|
||||
export const SQLITE_OPEN_NOMUTEX = 0x00008000;
|
||||
export const SQLITE_OPEN_FULLMUTEX = 0x00010000;
|
||||
export const SQLITE_OPEN_SHAREDCACHE = 0x00020000;
|
||||
export const SQLITE_OPEN_PRIVATECACHE = 0x00040000;
|
||||
export const SQLITE_OPEN_WAL = 0x00080000;
|
||||
export const SQLITE_OPEN_NOFOLLOW = 0x01000000;
|
||||
|
||||
// Locking levels.
|
||||
// https://www.sqlite.org/c3ref/c_lock_exclusive.html
|
||||
export const SQLITE_LOCK_NONE = 0;
|
||||
export const SQLITE_LOCK_SHARED = 1;
|
||||
export const SQLITE_LOCK_RESERVED = 2;
|
||||
export const SQLITE_LOCK_PENDING = 3;
|
||||
export const SQLITE_LOCK_EXCLUSIVE = 4;
|
||||
|
||||
// Device characteristics.
|
||||
// https://www.sqlite.org/c3ref/c_iocap_atomic.html
|
||||
export const SQLITE_IOCAP_ATOMIC = 0x00000001;
|
||||
export const SQLITE_IOCAP_ATOMIC512 = 0x00000002;
|
||||
export const SQLITE_IOCAP_ATOMIC1K = 0x00000004;
|
||||
export const SQLITE_IOCAP_ATOMIC2K = 0x00000008;
|
||||
export const SQLITE_IOCAP_ATOMIC4K = 0x00000010;
|
||||
export const SQLITE_IOCAP_ATOMIC8K = 0x00000020;
|
||||
export const SQLITE_IOCAP_ATOMIC16K = 0x00000040;
|
||||
export const SQLITE_IOCAP_ATOMIC32K = 0x00000080;
|
||||
export const SQLITE_IOCAP_ATOMIC64K = 0x00000100;
|
||||
export const SQLITE_IOCAP_SAFE_APPEND = 0x00000200;
|
||||
export const SQLITE_IOCAP_SEQUENTIAL = 0x00000400;
|
||||
export const SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN = 0x00000800;
|
||||
export const SQLITE_IOCAP_POWERSAFE_OVERWRITE = 0x00001000;
|
||||
export const SQLITE_IOCAP_IMMUTABLE = 0x00002000;
|
||||
export const SQLITE_IOCAP_BATCH_ATOMIC = 0x00004000;
|
||||
|
||||
// xAccess flags.
|
||||
// https://www.sqlite.org/c3ref/c_access_exists.html
|
||||
export const SQLITE_ACCESS_EXISTS = 0;
|
||||
export const SQLITE_ACCESS_READWRITE = 1;
|
||||
export const SQLITE_ACCESS_READ = 2;
|
||||
|
||||
// File control opcodes
|
||||
// https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlbeginatomicwrite
|
||||
export const SQLITE_FCNTL_LOCKSTATE = 1;
|
||||
export const SQLITE_FCNTL_GET_LOCKPROXYFILE = 2;
|
||||
export const SQLITE_FCNTL_SET_LOCKPROXYFILE = 3;
|
||||
export const SQLITE_FCNTL_LAST_ERRNO = 4;
|
||||
export const SQLITE_FCNTL_SIZE_HINT = 5;
|
||||
export const SQLITE_FCNTL_CHUNK_SIZE = 6;
|
||||
export const SQLITE_FCNTL_FILE_POINTER = 7;
|
||||
export const SQLITE_FCNTL_SYNC_OMITTED = 8;
|
||||
export const SQLITE_FCNTL_WIN32_AV_RETRY = 9;
|
||||
export const SQLITE_FCNTL_PERSIST_WAL = 10;
|
||||
export const SQLITE_FCNTL_OVERWRITE = 11;
|
||||
export const SQLITE_FCNTL_VFSNAME = 12;
|
||||
export const SQLITE_FCNTL_POWERSAFE_OVERWRITE = 13;
|
||||
export const SQLITE_FCNTL_PRAGMA = 14;
|
||||
export const SQLITE_FCNTL_BUSYHANDLER = 15;
|
||||
export const SQLITE_FCNTL_TEMPFILENAME = 16;
|
||||
export const SQLITE_FCNTL_MMAP_SIZE = 18;
|
||||
export const SQLITE_FCNTL_TRACE = 19;
|
||||
export const SQLITE_FCNTL_HAS_MOVED = 20;
|
||||
export const SQLITE_FCNTL_SYNC = 21;
|
||||
export const SQLITE_FCNTL_COMMIT_PHASETWO = 22;
|
||||
export const SQLITE_FCNTL_WIN32_SET_HANDLE = 23;
|
||||
export const SQLITE_FCNTL_WAL_BLOCK = 24;
|
||||
export const SQLITE_FCNTL_ZIPVFS = 25;
|
||||
export const SQLITE_FCNTL_RBU = 26;
|
||||
export const SQLITE_FCNTL_VFS_POINTER = 27;
|
||||
export const SQLITE_FCNTL_JOURNAL_POINTER = 28;
|
||||
export const SQLITE_FCNTL_WIN32_GET_HANDLE = 29;
|
||||
export const SQLITE_FCNTL_PDB = 30;
|
||||
export const SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = 31;
|
||||
export const SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = 32;
|
||||
export const SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = 33;
|
||||
export const SQLITE_FCNTL_LOCK_TIMEOUT = 34;
|
||||
export const SQLITE_FCNTL_DATA_VERSION = 35;
|
||||
export const SQLITE_FCNTL_SIZE_LIMIT = 36;
|
||||
export const SQLITE_FCNTL_CKPT_DONE = 37;
|
||||
export const SQLITE_FCNTL_RESERVE_BYTES = 38;
|
||||
export const SQLITE_FCNTL_CKPT_START = 39;
|
||||
|
||||
// Fundamental datatypes.
|
||||
// https://www.sqlite.org/c3ref/c_blob.html
|
||||
export const SQLITE_INTEGER = 1;
|
||||
export const SQLITE_FLOAT = 2;
|
||||
export const SQLITE_TEXT = 3;
|
||||
export const SQLITE_BLOB = 4;
|
||||
export const SQLITE_NULL = 5;
|
||||
|
||||
// Special destructor behavior.
|
||||
// https://www.sqlite.org/c3ref/c_static.html
|
||||
export const SQLITE_STATIC = 0;
|
||||
export const SQLITE_TRANSIENT = -1;
|
||||
|
||||
// Text encodings.
|
||||
// https://sqlite.org/c3ref/c_any.html
|
||||
export const SQLITE_UTF8 = 1; /* IMP: R-37514-35566 */
|
||||
export const SQLITE_UTF16LE = 2; /* IMP: R-03371-37637 */
|
||||
export const SQLITE_UTF16BE = 3; /* IMP: R-51971-34154 */
|
||||
export const SQLITE_UTF16 = 4; /* Use native byte order */
|
||||
|
||||
// Module constraint ops.
|
||||
export const SQLITE_INDEX_CONSTRAINT_EQ = 2;
|
||||
export const SQLITE_INDEX_CONSTRAINT_GT = 4;
|
||||
export const SQLITE_INDEX_CONSTRAINT_LE = 8;
|
||||
export const SQLITE_INDEX_CONSTRAINT_LT = 16;
|
||||
export const SQLITE_INDEX_CONSTRAINT_GE = 32;
|
||||
export const SQLITE_INDEX_CONSTRAINT_MATCH = 64;
|
||||
export const SQLITE_INDEX_CONSTRAINT_LIKE = 65;
|
||||
export const SQLITE_INDEX_CONSTRAINT_GLOB = 66;
|
||||
export const SQLITE_INDEX_CONSTRAINT_REGEXP = 67;
|
||||
export const SQLITE_INDEX_CONSTRAINT_NE = 68;
|
||||
export const SQLITE_INDEX_CONSTRAINT_ISNOT = 69;
|
||||
export const SQLITE_INDEX_CONSTRAINT_ISNOTNULL = 70;
|
||||
export const SQLITE_INDEX_CONSTRAINT_ISNULL = 71;
|
||||
export const SQLITE_INDEX_CONSTRAINT_IS = 72;
|
||||
export const SQLITE_INDEX_CONSTRAINT_FUNCTION = 150;
|
||||
export const SQLITE_INDEX_SCAN_UNIQUE = 1; /* Scan visits at most = 1 row */
|
||||
|
||||
// Function flags
|
||||
export const SQLITE_DETERMINISTIC = 0x000000800;
|
||||
export const SQLITE_DIRECTONLY = 0x000080000;
|
||||
export const SQLITE_SUBTYPE = 0x000100000;
|
||||
export const SQLITE_INNOCUOUS = 0x000200000;
|
||||
|
||||
// Sync flags
|
||||
export const SQLITE_SYNC_NORMAL = 0x00002;
|
||||
export const SQLITE_SYNC_FULL = 0x00003;
|
||||
export const SQLITE_SYNC_DATAONLY = 0x00010;
|
||||
|
||||
// Authorizer action codes
|
||||
export const SQLITE_CREATE_INDEX = 1;
|
||||
export const SQLITE_CREATE_TABLE = 2;
|
||||
export const SQLITE_CREATE_TEMP_INDEX = 3;
|
||||
export const SQLITE_CREATE_TEMP_TABLE = 4;
|
||||
export const SQLITE_CREATE_TEMP_TRIGGER = 5;
|
||||
export const SQLITE_CREATE_TEMP_VIEW = 6;
|
||||
export const SQLITE_CREATE_TRIGGER = 7;
|
||||
export const SQLITE_CREATE_VIEW = 8;
|
||||
export const SQLITE_DELETE = 9;
|
||||
export const SQLITE_DROP_INDEX = 10;
|
||||
export const SQLITE_DROP_TABLE = 11;
|
||||
export const SQLITE_DROP_TEMP_INDEX = 12;
|
||||
export const SQLITE_DROP_TEMP_TABLE = 13;
|
||||
export const SQLITE_DROP_TEMP_TRIGGER = 14;
|
||||
export const SQLITE_DROP_TEMP_VIEW = 15;
|
||||
export const SQLITE_DROP_TRIGGER = 16;
|
||||
export const SQLITE_DROP_VIEW = 17;
|
||||
export const SQLITE_INSERT = 18;
|
||||
export const SQLITE_PRAGMA = 19;
|
||||
export const SQLITE_READ = 20;
|
||||
export const SQLITE_SELECT = 21;
|
||||
export const SQLITE_TRANSACTION = 22;
|
||||
export const SQLITE_UPDATE = 23;
|
||||
export const SQLITE_ATTACH = 24;
|
||||
export const SQLITE_DETACH = 25;
|
||||
export const SQLITE_ALTER_TABLE = 26;
|
||||
export const SQLITE_REINDEX = 27;
|
||||
export const SQLITE_ANALYZE = 28;
|
||||
export const SQLITE_CREATE_VTABLE = 29;
|
||||
export const SQLITE_DROP_VTABLE = 30;
|
||||
export const SQLITE_FUNCTION = 31;
|
||||
export const SQLITE_SAVEPOINT = 32;
|
||||
export const SQLITE_COPY = 0;
|
||||
export const SQLITE_RECURSIVE = 33;
|
||||
|
||||
// Authorizer return codes
|
||||
export const SQLITE_DENY = 1;
|
||||
export const SQLITE_IGNORE = 2;
|
||||
|
||||
// Limit categories
|
||||
export const SQLITE_LIMIT_LENGTH = 0;
|
||||
export const SQLITE_LIMIT_SQL_LENGTH = 1;
|
||||
export const SQLITE_LIMIT_COLUMN = 2;
|
||||
export const SQLITE_LIMIT_EXPR_DEPTH = 3;
|
||||
export const SQLITE_LIMIT_COMPOUND_SELECT = 4;
|
||||
export const SQLITE_LIMIT_VDBE_OP = 5;
|
||||
export const SQLITE_LIMIT_FUNCTION_ARG = 6;
|
||||
export const SQLITE_LIMIT_ATTACHED = 7;
|
||||
export const SQLITE_LIMIT_LIKE_PATTERN_LENGTH = 8;
|
||||
export const SQLITE_LIMIT_VARIABLE_NUMBER = 9;
|
||||
export const SQLITE_LIMIT_TRIGGER_DEPTH = 10;
|
||||
export const SQLITE_LIMIT_WORKER_THREADS = 11;
|
||||
|
||||
export const SQLITE_PREPARE_PERSISTENT = 0x01;
|
||||
export const SQLITE_PREPARE_NORMALIZED = 0x02;
|
||||
export const SQLITE_PREPARE_NO_VTAB = 0x04;
|
||||
60
web/core/local-db/worker/wa-sqlite/src/types/globals.d.ts
vendored
Normal file
60
web/core/local-db/worker/wa-sqlite/src/types/globals.d.ts
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
declare namespace Asyncify {
|
||||
function handleAsync(f: () => Promise<any>);
|
||||
}
|
||||
|
||||
declare function UTF8ToString(ptr: number): string;
|
||||
declare function lengthBytesUTF8(s: string): number;
|
||||
declare function stringToUTF8(s: string, p: number, n: number);
|
||||
declare function ccall(name: string, returns: string, args: Array<any>, options?: object): any;
|
||||
declare function getValue(ptr: number, type: string): number;
|
||||
declare function setValue(ptr: number, value: number, type: string): number;
|
||||
declare function mergeInto(library: object, methods: object): void;
|
||||
|
||||
declare var HEAPU8: Uint8Array;
|
||||
declare var HEAPU32: Uint32Array;
|
||||
declare var LibraryManager;
|
||||
declare var Module;
|
||||
declare var _vfsAccess;
|
||||
declare var _vfsCheckReservedLock;
|
||||
declare var _vfsClose;
|
||||
declare var _vfsDelete;
|
||||
declare var _vfsDeviceCharacteristics;
|
||||
declare var _vfsFileControl;
|
||||
declare var _vfsFileSize;
|
||||
declare var _vfsLock;
|
||||
declare var _vfsOpen;
|
||||
declare var _vfsRead;
|
||||
declare var _vfsSectorSize;
|
||||
declare var _vfsSync;
|
||||
declare var _vfsTruncate;
|
||||
declare var _vfsUnlock;
|
||||
declare var _vfsWrite;
|
||||
|
||||
declare var _jsFunc;
|
||||
declare var _jsStep;
|
||||
declare var _jsFinal;
|
||||
|
||||
declare var _modStruct;
|
||||
declare var _modCreate;
|
||||
declare var _modConnect;
|
||||
declare var _modBestIndex;
|
||||
declare var _modDisconnect;
|
||||
declare var _modDestroy;
|
||||
declare var _modOpen;
|
||||
declare var _modClose;
|
||||
declare var _modFilter;
|
||||
declare var _modNext;
|
||||
declare var _modEof;
|
||||
declare var _modColumn;
|
||||
declare var _modRowid;
|
||||
declare var _modUpdate;
|
||||
declare var _modBegin;
|
||||
declare var _modSync;
|
||||
declare var _modCommit;
|
||||
declare var _modRollback;
|
||||
declare var _modFindFunction;
|
||||
declare var _modRename;
|
||||
|
||||
declare var _jsAuth;
|
||||
|
||||
declare var _jsProgress;
|
||||
1317
web/core/local-db/worker/wa-sqlite/src/types/index.d.ts
vendored
Normal file
1317
web/core/local-db/worker/wa-sqlite/src/types/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
16
web/core/local-db/worker/wa-sqlite/src/wa-sqlite.mjs
Normal file
16
web/core/local-db/worker/wa-sqlite/src/wa-sqlite.mjs
Normal file
File diff suppressed because one or more lines are too long
BIN
web/core/local-db/worker/wa-sqlite/src/wa-sqlite.wasm
Executable file
BIN
web/core/local-db/worker/wa-sqlite/src/wa-sqlite.wasm
Executable file
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
import { startSpan } from "@sentry/nextjs";
|
||||
// types
|
||||
import type {
|
||||
IIssueDisplayProperties,
|
||||
|
|
@ -14,7 +14,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
|
|||
import { persistence } from "@/local-db/storage.sqlite";
|
||||
// services
|
||||
|
||||
import { addIssue, addIssuesBulk, deleteIssueFromLocal } from "@/local-db/utils/load-issues";
|
||||
import { addIssuesBulk, deleteIssueFromLocal, updateIssue } from "@/local-db/utils/load-issues";
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class IssueService extends APIService {
|
||||
|
|
@ -63,7 +63,7 @@ export class IssueService extends APIService {
|
|||
}
|
||||
|
||||
async getIssues(workspaceSlug: string, projectId: string, queries?: any, config = {}): Promise<TIssuesResponse> {
|
||||
const response = await Sentry.startSpan({ name: "GET_ISSUES" }, async () => {
|
||||
const response = await startSpan({ name: "GET_ISSUES" }, async () => {
|
||||
const res = await persistence.getIssues(workspaceSlug, projectId, queries, config);
|
||||
return res;
|
||||
});
|
||||
|
|
@ -100,7 +100,7 @@ export class IssueService extends APIService {
|
|||
})
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
addIssue(response?.data);
|
||||
updateIssue({ ...response.data, is_local_update: 1 });
|
||||
}
|
||||
return response?.data;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore {
|
|||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(projectId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
|
|||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(cycleId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore {
|
|||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(projectId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
TStaticViewTypes,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
// helpers
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||
// lib
|
||||
|
|
@ -114,6 +114,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||
: nonEmptyArrayValue;
|
||||
});
|
||||
|
||||
if (displayFilters?.layout) issueFiltersParams.layout = displayFilters?.layout;
|
||||
|
||||
return issueFiltersParams;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore {
|
|||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(moduleId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
|
|||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(viewId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
|
|
@ -264,9 +265,16 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
|
|||
|
||||
const currentUserId = this.rootIssueStore.currentUserId;
|
||||
if (currentUserId)
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROJECT_VIEW, type, workspaceSlug, viewId, currentUserId, {
|
||||
this.handleIssuesLocalFilters.set(
|
||||
EIssuesStoreType.PROJECT_VIEW,
|
||||
type,
|
||||
workspaceSlug,
|
||||
viewId,
|
||||
currentUserId,
|
||||
{
|
||||
kanban_filters: _filters.kanbanFilters,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore {
|
|||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
getIssueFilters(projectId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:errors": "eslint . --ext .ts,.tsx --quiet",
|
||||
"export": "next export",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
||||
"dev:trace": "NEXT_TURBOPACK_TRACING=1 NEXT_CPU_PROF=1 next dev",
|
||||
"view-trace": "next internal turbo-trace-server ./.next/trace"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
|
||||
|
|
@ -34,10 +36,10 @@
|
|||
"@popperjs/core": "^2.11.8",
|
||||
"@react-pdf/renderer": "^3.4.5",
|
||||
"@sentry/nextjs": "^8.32.0",
|
||||
"@sqlite.org/sqlite-wasm": "^3.46.0-build2",
|
||||
"axios": "^1.7.4",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^1.0.0",
|
||||
"comlink": "^4.4.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"isomorphic-dompurify": "^2.12.0",
|
||||
|
|
@ -59,6 +61,7 @@
|
|||
"react-markdown": "^8.0.7",
|
||||
"react-pdf-html": "^2.1.2",
|
||||
"react-popper": "^2.3.0",
|
||||
"recharts": "^2.12.7",
|
||||
"sharp": "^0.32.1",
|
||||
"smooth-scroll-into-view-if-needed": "^2.0.2",
|
||||
"swr": "^2.1.3",
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
// The config you add here will be used whenever a users loads a page in their browser.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { replayIntegration, init } from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ Sentry.init({
|
|||
|
||||
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
replayIntegration({
|
||||
// Additional Replay configuration goes in here, for example:
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { init } from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
|
|
|
|||
18
yarn.lock
18
yarn.lock
|
|
@ -2935,15 +2935,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958"
|
||||
integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==
|
||||
|
||||
"@sqlite.org/sqlite-wasm@^3.46.0-build2":
|
||||
version "3.46.0-build2"
|
||||
resolved "https://registry.yarnpkg.com/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.46.0-build2.tgz#f84c3014f3fed6db08fc585d67e386d39e3956bf"
|
||||
integrity sha512-10s/u/Main1RGO+jjzK+mgC/zh1ls1CEnq3Dujr03TwvzLg+j4FAohOmlYkQj8KQOj1vGR9cuB9F8tVBTwVGVA==
|
||||
|
||||
"@storybook/addon-actions@8.3.6":
|
||||
version "8.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.3.6.tgz#80c5dbfc2278d72dc461a954bb729165ee1dfecb"
|
||||
integrity sha512-nOqgl0WoZK2KwjaABaXMoIgrIHOQl9inOzJvqQau0HOtsvnXGXYfJXYnpjZenoZDoZXKbUDl0U2haDFx2a2fJw==
|
||||
"@storybook/addon-actions@8.3.5":
|
||||
version "8.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.3.5.tgz#03fdb891114439ed47cb7df6ef21826530449db7"
|
||||
integrity sha512-t8D5oo+4XfD+F8091wLa2y/CDd/W2lExCeol5Vm1tp5saO+u6f2/d7iykLhTowWV84Uohi3D073uFeyTAlGebg==
|
||||
dependencies:
|
||||
"@storybook/global" "^5.0.0"
|
||||
"@types/uuid" "^9.0.1"
|
||||
|
|
@ -5416,6 +5411,11 @@ combined-stream@^1.0.8:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
comlink@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.4.1.tgz#e568b8e86410b809e8600eb2cf40c189371ef981"
|
||||
integrity sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==
|
||||
|
||||
comma-separated-tokens@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue