Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 83 additions & 59 deletions src/index/datacore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,24 @@ export class Datacore extends Component {
const init = (this.initializer = new DatacoreInitializer(this));
this.addChild(init);

await init.finished();
// Wait only for the cache phase to complete, then signal ready immediately.
await init.cacheReady();

this.initialized = true;
this.datastore.touch();
this.trigger("update", this.revision);
this.trigger("initialized");

// Continue waiting for background imports of stale/missing files.
await init.finished();

this.initializer = undefined;
this.removeChild(init);

this.datastore.touch();
this.trigger("update", this.revision);
this.trigger("initialized");

// Clean up any documents which no longer exist in the vault.
// TODO: I think this may race with other concurrent operations, so
// this may need to happen at the start of init and not at the end.
const currentFiles = this.vault.getFiles().map((file) => file.path);
this.persister.synchronize(currentFiles);
}
Expand Down Expand Up @@ -315,6 +320,12 @@ export class DatacoreInitializer extends Component {
current: TFile[];
/** Deferred promise which resolves when importing is done. */
done: Deferred<InitializationStats>;
/**
* Deferred promise which resolves as soon as all cached files have been loaded into the
* datastore. Files that are stale or missing from the cache continue importing in the
* background after this resolves.
*/
cacheReadyDeferred: Deferred<void>;

/** The total number of target files to import. */
targetTotal: number;
Expand All @@ -331,6 +342,9 @@ export class DatacoreInitializer extends Component {
/** Total number of cached files. */
cached: number;

/** Files that need a full background import (stale or not in cache). */
private backgroundQueue: TFile[];

constructor(public core: Datacore) {
super();

Expand All @@ -341,18 +355,32 @@ export class DatacoreInitializer extends Component {
this.start = Date.now();
this.current = [];
this.done = deferred();
this.cacheReadyDeferred = deferred();

this.backgroundQueue = [];

this.initialized = this.imported = this.skipped = this.cached = 0;
}

async onload() {
// Queue BATCH_SIZE elements from the queue to import.
this.active = true;

this.runNext();
// Phase 1: load everything available from the IndexedDB cache.
await this.loadFromCache();

// Signal that the index is usable — cached data is now in the datastore.
this.cacheReadyDeferred.resolve();

// Phase 2: import files that were stale or missing from the cache in the background.
this.runNextBackground();
}

/** Promise that resolves once all cached files have been loaded (plugin is usable). */
cacheReady(): Promise<void> {
return this.cacheReadyDeferred;
}

/** Promise which resolves when the initialization completes. */
/** Promise which resolves when the full initialization (including background imports) completes. */
finished(): Promise<InitializationStats> {
return this.done;
}
Expand All @@ -361,37 +389,70 @@ export class DatacoreInitializer extends Component {
onunload() {
if (this.active) {
this.active = false;
this.cacheReadyDeferred.resolve(); // unblock callers waiting on cache
this.done.reject("Initialization was cancelled before completing.");
}
}

/** Poll for another task to execute from the queue. */
private runNext() {
// Do nothing if max number of concurrent operations already running.
/** Phase 1: iterate all vault files and load valid cache entries synchronously (in batches). */
private async loadFromCache() {
const allFiles = this.queue.slice(); // snapshot
this.queue = [];

// Process in parallel batches to keep it fast without hammering IndexedDB.
const CACHE_BATCH = 32;
for (let i = 0; i < allFiles.length; i += CACHE_BATCH) {
if (!this.active) break;

const batch = allFiles.slice(i, i + CACHE_BATCH);
await Promise.all(
batch.map(async (file) => {
try {
const cached = await this.core.persister.loadFile(file.path);
if (cached && cached.time >= file.stat.mtime && cached.version === this.core.version) {
if (file.extension === "md") {
const data = MarkdownPage.from(cached.data as JsonMarkdownPage, (link) => link);
this.core.storeMarkdown(data);
this.cached++;
this.initialized++;
return;
}
}
// Cache miss or stale — queue for background import.
this.backgroundQueue.push(file);
} catch {
this.backgroundQueue.push(file);
}
})
);
}
}

/** Phase 2: import files that weren't in the cache, respecting BATCH_SIZE concurrency. */
private runNextBackground() {
if (!this.active || this.current.length >= DatacoreInitializer.BATCH_SIZE) {
return;
}

// There is space available to execute another.
const next = this.queue.pop();
const next = this.backgroundQueue.pop();
if (next) {
this.current.push(next);

// Run asynchronously to allow for concurrency.
(async () => {
try {
const result = await this.init(next);
this.handleResult(next, result);
} catch (error) {
this.handleResult(next, { status: "skipped" });
await this.core.reload(next);
this.imported++;
} catch {
this.skipped++;
}
this.initialized++;
this.current.remove(next);
this.runNextBackground();
})();

this.runNext();
} else if (!next && this.current.length == 0) {
this.runNextBackground();
} else if (this.current.length === 0) {
this.active = false;

// All work is done, resolve.
this.done.resolve({
durationMs: Date.now() - this.start,
files: this.files,
Expand All @@ -401,41 +462,6 @@ export class DatacoreInitializer extends Component {
});
}
}

/** Process the result of an initialization and queue more runs. */
private handleResult(file: TFile, result: InitializationResult) {
this.current.remove(file);
this.initialized++;

if (result.status === "skipped") this.skipped++;
else if (result.status === "imported") this.imported++;
else if (result.status === "cached") this.cached++;

// Queue more jobs for processing.
this.runNext();
}

/** Initialize a specific file. */
private async init(file: TFile): Promise<InitializationResult> {
try {
// Handle loading markdown files from cache.
const cached = await this.core.persister.loadFile(file.path);
if (cached && cached.time >= file.stat.mtime && cached.version == this.core.version) {
if (file.extension === "md") {
const data = MarkdownPage.from(cached.data as JsonMarkdownPage, (link) => link);
this.core.storeMarkdown(data);
return { status: "cached" };
}
}

// Does not match an existing import type, just reload normally.
await this.core.reload(file);
return { status: "imported" };
} catch (ex) {
console.log("Datacore: Failed to import file: ", ex);
return { status: "skipped" };
}
}
}

/** Statistics about a successful vault initialization. */
Expand All @@ -453,6 +479,4 @@ export interface InitializationStats {
}

/** The result of initializing a file. */
interface InitializationResult {
status: "skipped" | "imported" | "cached";
}
// Kept for potential future use.
108 changes: 108 additions & 0 deletions src/lang/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@

import { moment } from "obsidian";
import af from "./locale/af";
import ar from "./locale/ar";
import cz from "./locale/cz";
import bn from "./locale/bn";
import da from "./locale/da";
import de from "./locale/de";
import en from "./locale/en";
import enGB from "./locale/en-gb";
import es from "./locale/es";
import fr from "./locale/fr";
import hi from "./locale/hi";
import id from "./locale/id";
import it from "./locale/it";
import ja from "./locale/ja";
import ko from "./locale/ko";
import mr from "./locale/mr";
import nl from "./locale/nl";
import no from "./locale/no";
import pl from "./locale/pl";
import pt from "./locale/pt";
import ptBR from "./locale/pt-br";
import ro from "./locale/ro";
import ru from "./locale/ru";
import ta from "./locale/ta";
import te from "./locale/te";
import th from "./locale/th";
import tr from "./locale/tr";
import uk from "./locale/uk";
import ur from "./locale/ur";
import vi from "./locale/vi";
import zhCN from "./locale/zh-cn";
import zhTW from "./locale/zh-tw";

export const localeMap: { [k: string]: Partial<typeof en> } = {
af,
ar,
bn,
cs: cz,
da,
de,
en,
"en-gb": enGB,
es,
fr,
hi,
id,
it,
ja,
ko,
mr,
nl,
nn: no,
pl,
pt,
"pt-br": ptBR,
ro,
ru,
ta,
te,
th,
tr,
uk,
ur,
vi,
"zh-cn": zhCN,
"zh-tw": zhTW,
};

/**
* Get the Obsidian locale, falling back to a supported locale if needed
*/
function getObsidianLocale(): string {
const obsidianLocale = moment.locale();

if (localeMap[obsidianLocale]) {
return obsidianLocale;
}
const languageCode = obsidianLocale.split("-")[0];
if (localeMap[languageCode]) {
return languageCode;
}
return "en";
}

const locale = localeMap[getObsidianLocale()];

// https://stackoverflow.com/a/41015840/
function interpolate(str: string, params: Record<string, unknown>): string {
const names: string[] = Object.keys(params);
const vals: unknown[] = Object.values(params);
return new Function(...names, `return \`${str}\`;`)(...vals);
}

export function t(str: keyof typeof en, params?: Record<string, unknown>): string {
if (!locale) {
console.error(`Datacore: Locale ${getObsidianLocale()} not found.`);
}

const result = (locale && locale[str]) || en[str] || str;

if (params) {
return interpolate(result, params);
}

return result;
}
40 changes: 40 additions & 0 deletions src/lang/locale/af.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Afrikaans

export default {
// General
VIEWS: "Views",
FORMATTING: "Formatting",
PERFORMANCE: "Performance",

// Views settings
PAGINATION: "Pagination",
PAGINATION_DESC: "If enabled, splits up views into pages of results which can be traversed via buttons at the top and bottom of the view. This substantially improves the performance of large views, and can help with visual clutter. Note that this setting can also be set on a per-view basis.",
DEFAULT_PAGE_SIZE: "Default page size",
DEFAULT_PAGE_SIZE_DESC: "The number of entries to show per page, by default. This can be overriden on a per-view basis.",
SCROLL_ON_PAGE_CHANGE: "Scroll on page change",
SCROLL_ON_PAGE_CHANGE_DESC: "If enabled, table that are paged will scroll to the top of the table when the page changes. This can be overriden on a per-view basis.",

// Formatting settings
EMPTY_VALUES: "Empty values",
EMPTY_VALUES_DESC: "What to show for unset/empty properties.",
DEFAULT_DATE_FORMAT: "Default date format",
DEFAULT_DATE_FORMAT_DESC: "The default format that dates are rendered in. Uses luxon date formatting.",
DEFAULT_DATETIME_FORMAT: "Default date/time format",
DEFAULT_DATETIME_FORMAT_DESC: "The default format that date-times are rendered in. Uses luxon date formatting.",

// Performance settings
INLINE_FIELDS: "Inline fields",
INLINE_FIELDS_DESC: "If enabled, inline fields will be parsed in all documents. Finding inline fields requires a full text scan through each document, which noticably slows down indexing for large vaults. Disabling this functionality will mean metadata will only come from tags, links, and Properties / frontmatter",
IMPORTER_THREADS: "Importer threads",
IMPORTER_THREADS_DESC: "The number of importer threads to use for parsing metadata.",
IMPORTER_UTILIZATION: "Importer utilization",
IMPORTER_UTILIZATION_DESC: "How much CPU time each importer thread should use, as a fraction (0.1 - 1.0).",
MAX_RECURSIVE_RENDER_DEPTH: "Maximum recursive render depth",
MAX_RECURSIVE_RENDER_DEPTH_DESC: "Maximum depth that objects will be rendered to (i.e., how many levels of subproperties will be rendered by default). This avoids infinite recursion due to self-referential objects and ensures that rendering objects is acceptably performant.",

// Commands
REINDEX_VAULT: "Reindex entire vault",

// Loading
LOADING_TITLE: "Datacore is getting ready...",
VIEW_RENDERING: "< View is rendering >",};
Loading