Skip to content

Commit d747511

Browse files
committed
experimental new sync algorithm
1 parent 77e2ddd commit d747511

16 files changed

+219
-144
lines changed

exampleVault/examples fg.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ rating: 31
33
title: test title test test
44
completed: false
55
toggle1: false
6-
slider1: 6
7-
slider2: 6
8-
text1: Test
9-
text_area1: Test test
6+
slider1: 4
7+
slider2: 8
8+
text1: Testa
9+
text_area1: test test test
1010
date1: Wednesday, July 20th 2022
1111
select: option b
1212
nested:
1313
object: dfgdf
1414
multi_select:
15-
- option a
16-
- option c
15+
- option b
16+
- option d
1717
time1: 10:17
18-
suggest: "[[other note.md|other note]]"
18+
suggest: test
1919
---
2020

2121
## Components

exampleVault/other note.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
tags: test
3-
title: This is an example of how the meta bind
4-
select: option a
5-
date: Saturday, June 18th 2022
3+
title: "test test test "
4+
select: option d
5+
date: Tuesday, June 21st 2022
66
time: 19:20
77
multi-select:
88
- option b

src/InputFieldMarkdownRenderChild.ts

Lines changed: 31 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { InputFieldFactory } from './inputFields/InputFieldFactory';
55
import { InputFieldArgumentType, InputFieldDeclaration, InputFieldDeclarationParser } from './parsers/InputFieldDeclarationParser';
66
import { AbstractInputFieldArgument } from './inputFieldArguments/AbstractInputFieldArgument';
77
import { ClassInputFieldArgument } from './inputFieldArguments/ClassInputFieldArgument';
8-
import { getFrontmatterOfTFile, updateOrInsertFieldInTFile } from '@opd-libs/opd-metadata-lib/lib/API';
8+
import { getFrontmatterOfTFile } from '@opd-libs/opd-metadata-lib/lib/API';
99
import { traverseObject, validatePath as validateObjectPath } from '@opd-libs/opd-metadata-lib/lib/Utils';
1010
import { MetaBindBindTargetError, MetaBindInternalError } from './utils/MetaBindErrors';
1111

@@ -18,7 +18,7 @@ export class InputFieldMarkdownRenderChild extends MarkdownRenderChild {
1818
plugin: MetaBindPlugin;
1919
metaData: any;
2020
filePath: string;
21-
uid: number;
21+
uuid: string;
2222
inputField: AbstractInputField | undefined;
2323
error: string;
2424
type: InputFieldMarkdownRenderChildType;
@@ -28,17 +28,16 @@ export class InputFieldMarkdownRenderChild extends MarkdownRenderChild {
2828
bindTargetFile: TFile | undefined;
2929
bindTargetMetadataField: string | undefined;
3030

31-
limitInterval: number | undefined;
3231
intervalCounter: number;
3332
metadataValueUpdateQueue: any[];
3433
inputFieldValueUpdateQueue: any[];
3534

36-
constructor(containerEl: HTMLElement, type: InputFieldMarkdownRenderChildType, fullDeclaration: string, plugin: MetaBindPlugin, filePath: string, uid: number) {
35+
constructor(containerEl: HTMLElement, type: InputFieldMarkdownRenderChildType, fullDeclaration: string, plugin: MetaBindPlugin, filePath: string, uuid: string) {
3736
super(containerEl);
3837

3938
this.error = '';
4039
this.filePath = filePath;
41-
this.uid = uid;
40+
this.uuid = uuid;
4241
this.plugin = plugin;
4342
this.type = type;
4443
this.fullDeclaration = fullDeclaration;
@@ -58,10 +57,8 @@ export class InputFieldMarkdownRenderChild extends MarkdownRenderChild {
5857
this.inputField = InputFieldFactory.createInputField(this.inputFieldDeclaration.inputFieldType, {
5958
type: type,
6059
inputFieldMarkdownRenderChild: this,
61-
onValueChanged: this.pushToMetadataValueUpdateQueue.bind(this),
60+
onValueChanged: this.updateMetadataManager.bind(this),
6261
});
63-
64-
this.limitInterval = window.setInterval(() => this.applyValueUpdateQueues(), this.plugin.settings.syncInterval);
6562
} catch (e: any) {
6663
this.error = e.message;
6764
console.warn(e);
@@ -109,85 +106,47 @@ export class InputFieldMarkdownRenderChild extends MarkdownRenderChild {
109106
}
110107
}
111108

112-
// use this interval to reduce writing operations
113-
async applyValueUpdateQueues(): Promise<void> {
114-
if (this.metadataValueUpdateQueue.length !== 0) {
115-
console.debug(`meta-bind | applying to metadataUpdateQueue to field ${this.uid}`);
116-
await this.applyMetadataValueUpdateQueue();
117-
this.cleanUpUpdateQueues();
109+
registerSelfToMetadataManager(): void {
110+
if (!this.inputFieldDeclaration?.isBound || !this.bindTargetFile || !this.bindTargetMetadataField) {
118111
return;
119112
}
120113

121-
if (this.inputFieldValueUpdateQueue.length !== 0) {
122-
console.debug(`meta-bind | applying to inputFieldValueUpdateQueue to field ${this.uid}`);
123-
await this.applyInputFieldValueUpdateQueue();
124-
this.cleanUpUpdateQueues();
125-
return;
126-
}
114+
this.plugin.metadataManager.register(
115+
this.bindTargetFile,
116+
metadata => {
117+
if (!this.inputField) {
118+
throw new MetaBindInternalError('inputField is undefined, can not update inputField');
119+
}
120+
121+
const value = traverseObject(this.bindTargetMetadataField as string, metadata);
122+
if (!this.inputField.isEqualValue(value)) {
123+
this.inputField.setValue(value);
124+
}
125+
},
126+
this.uuid
127+
);
127128
}
128129

129-
async applyMetadataValueUpdateQueue(): Promise<void> {
130-
if (!this.inputFieldDeclaration) {
131-
throw new MetaBindInternalError('inputFieldDeclaration is undefined, can not update metadata');
132-
}
133-
if (!this.inputFieldDeclaration.isBound) {
130+
unregisterSelfFromMetadataManager(): void {
131+
if (!this.inputFieldDeclaration?.isBound || !this.bindTargetFile || !this.bindTargetMetadataField) {
134132
return;
135133
}
136-
if (!this.bindTargetMetadataField || !this.bindTargetFile) {
137-
throw new MetaBindInternalError('bindTargetMetadataField or bindTargetFile is undefined, can not update metadata');
138-
}
139-
140-
if (this.metadataValueUpdateQueue.length > 0) {
141-
await updateOrInsertFieldInTFile(this.bindTargetMetadataField, this.metadataValueUpdateQueue.at(-1), this.bindTargetFile, this.plugin);
142-
} else {
143-
throw new MetaBindInternalError(`cannot apply metadataValueUpdateQueue to inputField ${this.uid}, metadataValueUpdateQueue is empty`);
144-
}
145-
}
146-
147-
async applyInputFieldValueUpdateQueue(): Promise<void> {
148-
if (!this.inputFieldDeclaration) {
149-
throw new MetaBindInternalError('inputFieldDeclaration is undefined, can not update inputField');
150-
}
151-
if (!this.inputField) {
152-
throw new MetaBindInternalError('inputField is undefined, can not update inputField');
153-
}
154-
155-
if (this.inputFieldValueUpdateQueue.length > 0) {
156-
let value = this.inputFieldValueUpdateQueue.at(-1);
157-
158-
if (value == null) {
159-
value = this.inputField.getDefaultValue();
160-
}
161-
162-
this.inputField.setValue(value);
163-
} else {
164-
throw new MetaBindInternalError(`cannot apply inputFieldValueUpdateQueue to inputField ${this.uid}, inputFieldValueUpdateQueue is empty`);
165-
}
166-
}
167134

168-
cleanUpUpdateQueues(): void {
169-
this.metadataValueUpdateQueue = [];
170-
this.inputFieldValueUpdateQueue = [];
135+
this.plugin.metadataManager.unregister(this.bindTargetFile, this.uuid);
171136
}
172137

173-
pushToMetadataValueUpdateQueue(value: any): void {
174-
if (this.inputFieldDeclaration?.isBound) {
175-
console.debug(`meta-bind | pushed value ${value} (typeof ${typeof value}) to metadataUpdateQueue on field ${this.uid}`);
176-
this.metadataValueUpdateQueue.push(value);
138+
updateMetadataManager(value: any): void {
139+
if (!this.inputFieldDeclaration?.isBound || !this.bindTargetFile || !this.bindTargetMetadataField) {
140+
return;
177141
}
178-
}
179142

180-
pushToInputFieldValueUpdateQueue(value: any): void {
181-
if (!this.inputField?.isEqualValue(value)) {
182-
console.debug(`meta-bind | pushed value ${value} (typeof ${typeof value}) to inputFieldValueUpdateQueue on field ${this.uid}`);
183-
this.inputFieldValueUpdateQueue.push(value);
184-
}
143+
this.plugin.metadataManager.updatePropertyInMetadataFileCache(value, this.bindTargetMetadataField, this.bindTargetFile, this.uuid);
185144
}
186145

187146
getInitialValue(): any | undefined {
188147
if (this.inputFieldDeclaration?.isBound && this.bindTargetMetadataField) {
189148
const value = traverseObject(this.bindTargetMetadataField, this.metaData);
190-
console.debug(`meta-bind | setting initial value to ${value} (typeof ${typeof value}) for input field ${this.uid}`);
149+
console.debug(`meta-bind | setting initial value to ${value} (typeof ${typeof value}) for input field ${this.uuid}`);
191150
return value ?? this.inputField?.getDefaultValue();
192151
}
193152
}
@@ -231,6 +190,7 @@ export class InputFieldMarkdownRenderChild extends MarkdownRenderChild {
231190
return;
232191
}
233192

193+
this.registerSelfToMetadataManager();
234194
this.plugin.registerInputFieldMarkdownRenderChild(this);
235195

236196
this.inputField.render(container);
@@ -248,9 +208,8 @@ export class InputFieldMarkdownRenderChild extends MarkdownRenderChild {
248208
console.debug('meta-bind | unload inputFieldMarkdownRenderChild', this);
249209

250210
this.plugin.unregisterInputFieldMarkdownRenderChild(this);
211+
this.unregisterSelfFromMetadataManager();
251212

252213
super.onunload();
253-
254-
window.clearInterval(this.limitInterval);
255214
}
256215
}

src/MetadataManager.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { CachedMetadata, TFile } from 'obsidian';
2+
import MetaBindPlugin from './main';
3+
import { setFrontmatterOfTFile } from '@opd-libs/opd-metadata-lib/lib/API';
4+
import { traverseObjectToParent } from '@opd-libs/opd-metadata-lib/lib/Utils';
5+
import { Internal } from '@opd-libs/opd-metadata-lib/lib/Internal';
6+
import getMetaDataFromFileContent = Internal.getMetaDataFromFileContent;
7+
8+
export interface MetadataFileCache {
9+
file: TFile;
10+
metadata: Record<string, any>;
11+
listeners: {
12+
onCacheUpdate: (metadata: Record<string, any>) => void;
13+
uuid: string;
14+
}[];
15+
cyclesSinceLastUpdate: number;
16+
changed: boolean;
17+
}
18+
19+
export class MetadataManager {
20+
cache: MetadataFileCache[];
21+
plugin: MetaBindPlugin;
22+
updateCycleThreshold = 5;
23+
interval: number | undefined;
24+
25+
constructor(plugin: MetaBindPlugin) {
26+
this.plugin = plugin;
27+
28+
this.cache = [];
29+
30+
this.plugin.registerEvent(this.plugin.app.metadataCache.on('changed', this.updateMetadataFileCacheOnFrontmatterUpdate.bind(this)));
31+
32+
this.interval = window.setInterval(() => this.update(), this.plugin.settings.syncInterval);
33+
}
34+
35+
register(file: TFile, onCacheUpdate: (metadata: Record<string, any>) => void, uuid: string): void {
36+
const fileCache = this.getCacheForFile(file);
37+
if (fileCache) {
38+
console.debug(`meta-bind | registered ${uuid} to existing file cache`);
39+
fileCache.listeners.push({ onCacheUpdate, uuid });
40+
} else {
41+
const c = {
42+
file: file,
43+
metadata: {},
44+
listeners: [{ onCacheUpdate, uuid }],
45+
cyclesSinceLastUpdate: 0,
46+
changed: false,
47+
}
48+
49+
this.plugin.app.vault.cachedRead(file).then(value => c.metadata = getMetaDataFromFileContent(value));
50+
51+
this.cache.push(c);
52+
}
53+
}
54+
55+
unregister(file: TFile, uuid: string): void {
56+
const fileCache = this.getCacheForFile(file);
57+
if (!fileCache) {
58+
return;
59+
}
60+
61+
fileCache.listeners = fileCache.listeners.filter(x => x.uuid !== uuid);
62+
if (fileCache.listeners.length === 0) {
63+
this.cache = this.cache.filter(x => x.file.path !== file.path);
64+
}
65+
}
66+
67+
getCacheForFile(file: TFile): MetadataFileCache | undefined {
68+
return this.cache.find(x => x.file.path === file.path);
69+
}
70+
71+
private update(): void {
72+
// console.debug('meta-bind | updating metadata manager');
73+
74+
for (const metadataFileCache of this.cache) {
75+
if (metadataFileCache.changed) {
76+
this.updateFrontmatter(metadataFileCache);
77+
}
78+
metadataFileCache.cyclesSinceLastUpdate += 1;
79+
}
80+
}
81+
82+
async updateFrontmatter(fileCache: MetadataFileCache): Promise<void> {
83+
fileCache.changed = false;
84+
await setFrontmatterOfTFile(fileCache.metadata, fileCache.file, this.plugin);
85+
}
86+
87+
updateMetadataFileCache(metadata: Record<string, any>, file: TFile, uuid?: string | undefined): void {
88+
const fileCache = this.getCacheForFile(file);
89+
if (!fileCache) {
90+
return;
91+
}
92+
93+
fileCache.metadata = metadata;
94+
fileCache.cyclesSinceLastUpdate = 0;
95+
96+
this.notifyListeners(fileCache, uuid);
97+
}
98+
99+
updatePropertyInMetadataFileCache(value: any, path: string, file: TFile, uuid?: string | undefined): void {
100+
console.debug(`meta-bind | updating ${path} in ${file.path} metadata cache to`, value);
101+
102+
const fileCache = this.getCacheForFile(file);
103+
if (!fileCache) {
104+
return;
105+
}
106+
107+
const { parent, child } = traverseObjectToParent(path, fileCache.metadata);
108+
109+
if (parent.value === undefined) {
110+
throw Error(`The parent of "${path}" does not exist in Object, please create the parent first`);
111+
}
112+
113+
parent.value[child.key] = value;
114+
fileCache.cyclesSinceLastUpdate = 0;
115+
fileCache.changed = true;
116+
117+
this.notifyListeners(fileCache, uuid);
118+
}
119+
120+
updateMetadataFileCacheOnFrontmatterUpdate(file: TFile, data: string, cache: CachedMetadata): void {
121+
const fileCache = this.getCacheForFile(file);
122+
if (!fileCache) {
123+
return;
124+
}
125+
126+
if (fileCache.cyclesSinceLastUpdate < this.updateCycleThreshold) {
127+
return;
128+
}
129+
130+
const metadata: Record<string, any> = Object.assign({}, cache.frontmatter); // copy
131+
delete metadata.position;
132+
133+
console.debug(`meta-bind | updating ${file} on frontmatter update`, metadata);
134+
135+
fileCache.metadata = metadata;
136+
fileCache.cyclesSinceLastUpdate = 0;
137+
fileCache.changed = true;
138+
139+
this.notifyListeners(fileCache);
140+
}
141+
142+
notifyListeners(fileCache: MetadataFileCache, exceptUuid?: string | undefined): void {
143+
for (const listener of fileCache.listeners) {
144+
if (exceptUuid && exceptUuid === listener.uuid) {
145+
continue;
146+
}
147+
listener.onCacheUpdate(fileCache.metadata);
148+
}
149+
}
150+
}

src/inputFields/DateInputField.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class DateInputField extends AbstractInputField {
6464

6565
this.date = DateParser.parse(value);
6666
if (!this.date) {
67-
console.warn(new MetaBindValueError(`invalid value '${value}' at dateInputField ${this.inputFieldMarkdownRenderChild.uid}`));
67+
console.warn(new MetaBindValueError(`invalid value '${value}' at dateInputField ${this.inputFieldMarkdownRenderChild.uuid}`));
6868
this.date = DateParser.getDefaultDate();
6969
}
7070

@@ -88,7 +88,7 @@ export class DateInputField extends AbstractInputField {
8888
}
8989

9090
public render(container: HTMLDivElement): void {
91-
console.debug(`meta-bind | render dateInputField ${this.inputFieldMarkdownRenderChild.uid}`);
91+
console.debug(`meta-bind | render dateInputField ${this.inputFieldMarkdownRenderChild.uuid}`);
9292

9393
this.date = DateParser.parse(this.inputFieldMarkdownRenderChild.getInitialValue()) ?? DateParser.getDefaultDate();
9494
if (!this.date.isValid()) {

src/inputFields/DatePicker/DatePickerInputField.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class DatePickerInputField extends AbstractInputField {
6767
}
6868

6969
render(container: HTMLDivElement): void {
70-
console.debug(`meta-bind | render datePickerInputField ${this.inputFieldMarkdownRenderChild.uid}`);
70+
console.debug(`meta-bind | render datePickerInputField ${this.inputFieldMarkdownRenderChild.uuid}`);
7171

7272
this.container = container;
7373

0 commit comments

Comments
 (0)