@lapis-notes/api 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -14
- package/dist/components/editor/editor.svelte +3 -2
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +6 -11
- package/dist/editor-save-context.d.ts +24 -0
- package/dist/editor-save-context.js +101 -0
- package/dist/editor.svelte.d.ts +19 -1
- package/dist/editor.svelte.js +416 -34
- package/dist/file-editing-sessions.d.ts +28 -0
- package/dist/file-editing-sessions.js +197 -0
- package/dist/file-manager.d.ts +1 -0
- package/dist/file-manager.js +8 -0
- package/dist/generated/plugin-host-modules.schema.generated.d.ts +64 -34
- package/dist/generated/plugin-host-modules.schema.generated.js +67 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/metadata-store.d.ts +23 -0
- package/dist/metadata-store.js +1 -0
- package/dist/plugin-manager.d.ts +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +17 -5
- package/dist/storage/app-database.d.ts +3 -21
- package/dist/sync-editor-flush.d.ts +6 -0
- package/dist/sync-editor-flush.js +22 -0
- package/dist/sync-host-local.d.ts +15 -0
- package/dist/sync-host-local.js +228 -0
- package/dist/sync-host-security.d.ts +24 -9
- package/dist/sync-host-security.js +47 -14
- package/dist/sync-host.d.ts +23 -3
- package/dist/sync-host.js +155 -9
- package/dist/sync-repository.d.ts +106 -0
- package/dist/sync-repository.js +18 -0
- package/dist/view.svelte.js +28 -19
- package/package.json +17 -1
package/dist/sync-host.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TFile } from "./storage/fs";
|
|
2
2
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
3
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
4
|
+
import { ScopedSyncLocalRuntime } from "./sync-host-local";
|
|
4
5
|
import { SyncHostSecurityService, } from "./sync-host-security";
|
|
5
6
|
const ROOT_PREFIX = "sync-host:v1:";
|
|
6
7
|
const DATABASE_GENERATION_KEY = `${ROOT_PREFIX}database-generation`;
|
|
@@ -71,19 +72,39 @@ export class SyncHostService {
|
|
|
71
72
|
database;
|
|
72
73
|
vault;
|
|
73
74
|
clock;
|
|
74
|
-
options;
|
|
75
75
|
#ready;
|
|
76
76
|
#eventQueue = Promise.resolve();
|
|
77
77
|
#runtimeIds = new Map();
|
|
78
78
|
#origins = new Map();
|
|
79
79
|
#security;
|
|
80
|
+
#selectSecurityProvider;
|
|
81
|
+
#securityProviders = new Map();
|
|
82
|
+
#delegatedRuntimes = new Map();
|
|
83
|
+
#localRuntimes = new Map();
|
|
84
|
+
#selectLocalProvider;
|
|
85
|
+
#guardLocalApply;
|
|
86
|
+
#securityCleanup = new Set();
|
|
87
|
+
#stopObserving;
|
|
88
|
+
#disposed = false;
|
|
89
|
+
#disposal;
|
|
90
|
+
options;
|
|
80
91
|
constructor(database, vault, clock = Date.now, options = {}) {
|
|
81
92
|
this.database = database;
|
|
82
93
|
this.vault = vault;
|
|
83
94
|
this.clock = clock;
|
|
84
|
-
|
|
85
|
-
this
|
|
86
|
-
|
|
95
|
+
const { securityStore, securityProvider, localProvider, guardLocalApply, ...retainedOptions } = options;
|
|
96
|
+
this.options =
|
|
97
|
+
securityStore === undefined &&
|
|
98
|
+
securityProvider === undefined &&
|
|
99
|
+
localProvider === undefined &&
|
|
100
|
+
guardLocalApply === undefined
|
|
101
|
+
? options
|
|
102
|
+
: retainedOptions;
|
|
103
|
+
this.#selectSecurityProvider = securityProvider;
|
|
104
|
+
this.#selectLocalProvider = localProvider;
|
|
105
|
+
this.#guardLocalApply = guardLocalApply;
|
|
106
|
+
this.#security = new SyncHostSecurityService(securityStore === undefined ? database : securityStore, (pluginId, runtimeId) => this.#assertRuntime(pluginId, runtimeId), options.security);
|
|
107
|
+
const eventRef = this.vault.on("all", (event, file, context) => {
|
|
87
108
|
if (!(file instanceof TFile) || !this.#isChangeKind(event))
|
|
88
109
|
return;
|
|
89
110
|
const previousPath = typeof context.oldPath === "string" ? context.oldPath : undefined;
|
|
@@ -96,6 +117,7 @@ export class SyncHostService {
|
|
|
96
117
|
})
|
|
97
118
|
.catch(() => undefined);
|
|
98
119
|
});
|
|
120
|
+
this.#stopObserving = () => this.vault.offref(eventRef);
|
|
99
121
|
this.#ready = this.#initialize();
|
|
100
122
|
}
|
|
101
123
|
async ready() {
|
|
@@ -103,6 +125,8 @@ export class SyncHostService {
|
|
|
103
125
|
await this.#eventQueue;
|
|
104
126
|
}
|
|
105
127
|
forPlugin(pluginId, grantedCapabilities) {
|
|
128
|
+
if (this.#disposed)
|
|
129
|
+
throw new Error("Sync host is disposed");
|
|
106
130
|
const normalizedPluginId = requireBoundedString(pluginId, "Plugin ID", 128);
|
|
107
131
|
const runtimeId = randomId();
|
|
108
132
|
const runtimeIds = this.#runtimeIds.get(normalizedPluginId) ?? new Set();
|
|
@@ -116,7 +140,7 @@ export class SyncHostService {
|
|
|
116
140
|
throw new Error(`Plugin ${normalizedPluginId} is not granted ${capability}`);
|
|
117
141
|
}
|
|
118
142
|
};
|
|
119
|
-
|
|
143
|
+
const capabilities = {
|
|
120
144
|
files: {
|
|
121
145
|
getIdentity: async (path) => {
|
|
122
146
|
assertGranted("metadata:identity");
|
|
@@ -304,32 +328,154 @@ export class SyncHostService {
|
|
|
304
328
|
}),
|
|
305
329
|
generatedState: this.#generatedState(normalizedPluginId, runtimeId, () => assertGranted("plugin:generated-state")),
|
|
306
330
|
};
|
|
331
|
+
if (this.#selectLocalProvider) {
|
|
332
|
+
const local = new ScopedSyncLocalRuntime({
|
|
333
|
+
runtimeId,
|
|
334
|
+
assertGranted,
|
|
335
|
+
selectProvider: () => this.#selectLocalProvider(normalizedPluginId),
|
|
336
|
+
guardApply: this.#guardLocalApply,
|
|
337
|
+
});
|
|
338
|
+
this.#localRuntimes.set(runtimeId, local);
|
|
339
|
+
Object.assign(capabilities, { local: local.capabilities });
|
|
340
|
+
}
|
|
341
|
+
let delegated;
|
|
342
|
+
let selected = false;
|
|
343
|
+
for (const group of [
|
|
344
|
+
"crypto",
|
|
345
|
+
"syncIdentity",
|
|
346
|
+
"pairing",
|
|
347
|
+
"membership",
|
|
348
|
+
"recovery",
|
|
349
|
+
]) {
|
|
350
|
+
const local = capabilities[group];
|
|
351
|
+
const guarded = {};
|
|
352
|
+
for (const method of Object.keys(local)) {
|
|
353
|
+
guarded[method] = async (...args) => {
|
|
354
|
+
assertGranted("crypto:nostr-sync");
|
|
355
|
+
if (!selected) {
|
|
356
|
+
const provider = this.#securityProvider(normalizedPluginId);
|
|
357
|
+
if (provider) {
|
|
358
|
+
this.#delegatedRuntimes.set(runtimeId, provider);
|
|
359
|
+
delegated = provider.forRuntime(runtimeId);
|
|
360
|
+
if (!delegated || typeof delegated !== "object")
|
|
361
|
+
throw new Error("Invalid Sync security provider capabilities");
|
|
362
|
+
}
|
|
363
|
+
selected = true;
|
|
364
|
+
}
|
|
365
|
+
assertGranted("crypto:nostr-sync");
|
|
366
|
+
const target = delegated ? delegated[group] : local;
|
|
367
|
+
const operation = target && target[method];
|
|
368
|
+
if (typeof operation !== "function")
|
|
369
|
+
throw new Error(`Invalid Sync security provider operation: ${group}.${method}`);
|
|
370
|
+
const result = await operation.apply(target, args);
|
|
371
|
+
assertGranted("crypto:nostr-sync");
|
|
372
|
+
return result;
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
Object.assign(capabilities, { [group]: guarded });
|
|
376
|
+
}
|
|
377
|
+
return capabilities;
|
|
307
378
|
}
|
|
308
379
|
revokePlugin(pluginId) {
|
|
380
|
+
const runtimes = this.#runtimeIds.get(pluginId);
|
|
309
381
|
this.#runtimeIds.delete(pluginId);
|
|
310
382
|
for (const [token, owner] of this.#origins) {
|
|
311
383
|
if (owner.pluginId === pluginId)
|
|
312
384
|
this.#origins.delete(token);
|
|
313
385
|
}
|
|
314
386
|
this.#security.revokePlugin(pluginId);
|
|
387
|
+
const cleanups = [];
|
|
388
|
+
for (const runtimeId of runtimes ?? []) {
|
|
389
|
+
const provider = this.#delegatedRuntimes.get(runtimeId);
|
|
390
|
+
this.#delegatedRuntimes.delete(runtimeId);
|
|
391
|
+
const local = this.#localRuntimes.get(runtimeId);
|
|
392
|
+
this.#localRuntimes.delete(runtimeId);
|
|
393
|
+
if (!provider && !local)
|
|
394
|
+
continue;
|
|
395
|
+
let cleanup;
|
|
396
|
+
try {
|
|
397
|
+
// Local streams/operations must finish while native crypto custody still exists.
|
|
398
|
+
const localCleanup = local?.revoke();
|
|
399
|
+
cleanup = localCleanup
|
|
400
|
+
? (async () => {
|
|
401
|
+
const result = await Promise.allSettled([localCleanup]);
|
|
402
|
+
await provider?.revokeRuntime(runtimeId);
|
|
403
|
+
if (result[0]?.status === "rejected")
|
|
404
|
+
throw result[0].reason;
|
|
405
|
+
})()
|
|
406
|
+
: Promise.resolve(provider.revokeRuntime(runtimeId));
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
cleanup = Promise.reject(error);
|
|
410
|
+
}
|
|
411
|
+
this.#securityCleanup.add(cleanup);
|
|
412
|
+
cleanups.push(cleanup);
|
|
413
|
+
}
|
|
414
|
+
return this.#drainSecurity(cleanups);
|
|
415
|
+
}
|
|
416
|
+
/** Invalidate first, then wait for every admitted host cleanup and file event. */
|
|
417
|
+
dispose() {
|
|
418
|
+
if (this.#disposal)
|
|
419
|
+
return this.#disposal;
|
|
420
|
+
this.#disposed = true;
|
|
421
|
+
this.#stopObserving();
|
|
422
|
+
const revocations = [...this.#runtimeIds.keys()].map((pluginId) => this.revokePlugin(pluginId));
|
|
423
|
+
this.#disposal = this.#drainSecurity([
|
|
424
|
+
...revocations,
|
|
425
|
+
...this.#securityCleanup,
|
|
426
|
+
this.ready(),
|
|
427
|
+
]);
|
|
428
|
+
return this.#disposal;
|
|
429
|
+
}
|
|
430
|
+
async #drainSecurity(cleanups) {
|
|
431
|
+
const results = await Promise.allSettled(cleanups);
|
|
432
|
+
for (const cleanup of cleanups)
|
|
433
|
+
this.#securityCleanup.delete(cleanup);
|
|
434
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
435
|
+
if (failed?.status === "rejected")
|
|
436
|
+
throw failed.reason;
|
|
437
|
+
}
|
|
438
|
+
#securityProvider(pluginId) {
|
|
439
|
+
if (!this.#securityProviders.has(pluginId)) {
|
|
440
|
+
const provider = this.#selectSecurityProvider?.(pluginId);
|
|
441
|
+
if (provider !== undefined &&
|
|
442
|
+
(!provider ||
|
|
443
|
+
typeof provider.forRuntime !== "function" ||
|
|
444
|
+
typeof provider.revokeRuntime !== "function"))
|
|
445
|
+
throw new Error("Invalid Sync security provider");
|
|
446
|
+
this.#securityProviders.set(pluginId, provider);
|
|
447
|
+
}
|
|
448
|
+
return this.#securityProviders.get(pluginId);
|
|
449
|
+
}
|
|
450
|
+
#assertLocalSecurity(pluginId) {
|
|
451
|
+
requireBoundedString(pluginId, "Plugin ID", 128);
|
|
452
|
+
if (this.#disposed)
|
|
453
|
+
throw new Error("Sync host is disposed");
|
|
454
|
+
if (this.#securityProvider(pluginId))
|
|
455
|
+
throw new Error(`Sync security for ${pluginId} is delegated to its host provider`);
|
|
315
456
|
}
|
|
316
457
|
/** Host-controlled enrollment; this operation is intentionally absent from plugin facades. */
|
|
317
|
-
enrollPluginCrypto(pluginId, passphrase, request) {
|
|
458
|
+
async enrollPluginCrypto(pluginId, passphrase, request) {
|
|
459
|
+
this.#assertLocalSecurity(pluginId);
|
|
318
460
|
return this.#security.enroll(requireBoundedString(pluginId, "Plugin ID", 128), passphrase, request);
|
|
319
461
|
}
|
|
320
462
|
/** Host-controlled unlock; plugins can only observe locked state. */
|
|
321
|
-
unlockPluginCrypto(pluginId, passphrase) {
|
|
463
|
+
async unlockPluginCrypto(pluginId, passphrase) {
|
|
464
|
+
this.#assertLocalSecurity(pluginId);
|
|
322
465
|
return this.#security.unlock(requireBoundedString(pluginId, "Plugin ID", 128), passphrase);
|
|
323
466
|
}
|
|
324
467
|
/** Host-controlled passkey or OS-credential enrollment. */
|
|
325
|
-
enrollPluginCryptoWithWrappingKey(pluginId, wrappingKey, descriptor, request) {
|
|
468
|
+
async enrollPluginCryptoWithWrappingKey(pluginId, wrappingKey, descriptor, request) {
|
|
469
|
+
this.#assertLocalSecurity(pluginId);
|
|
326
470
|
return this.#security.enrollWithWrappingKey(requireBoundedString(pluginId, "Plugin ID", 128), wrappingKey, descriptor, request);
|
|
327
471
|
}
|
|
328
472
|
/** Host-controlled passkey or OS-credential unlock. */
|
|
329
|
-
unlockPluginCryptoWithWrappingKey(pluginId, wrappingKey) {
|
|
473
|
+
async unlockPluginCryptoWithWrappingKey(pluginId, wrappingKey) {
|
|
474
|
+
this.#assertLocalSecurity(pluginId);
|
|
330
475
|
return this.#security.unlockWithWrappingKey(requireBoundedString(pluginId, "Plugin ID", 128), wrappingKey);
|
|
331
476
|
}
|
|
332
477
|
lockPluginCrypto(pluginId) {
|
|
478
|
+
this.#assertLocalSecurity(pluginId);
|
|
333
479
|
this.#security.lock(requireBoundedString(pluginId, "Plugin ID", 128));
|
|
334
480
|
}
|
|
335
481
|
grantPluginNetworkOrigin(pluginId, origin, options) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Durable opaque sync data. Hosts keep this separate from disposable AppDatabase indexes. */
|
|
2
|
+
export interface SealedSyncState {
|
|
3
|
+
version: number;
|
|
4
|
+
epoch: number;
|
|
5
|
+
ciphertext: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
export interface SyncRepository {
|
|
8
|
+
readonly blobs: {
|
|
9
|
+
has(hash: string): Promise<boolean>;
|
|
10
|
+
put(ciphertext: Uint8Array): Promise<{
|
|
11
|
+
hash: string;
|
|
12
|
+
size: number;
|
|
13
|
+
}>;
|
|
14
|
+
get(hash: string): Promise<Uint8Array>;
|
|
15
|
+
};
|
|
16
|
+
readState(): Promise<SealedSyncState | null>;
|
|
17
|
+
compareAndSwap(expectedVersion: number, next: SealedSyncState, authority?: SyncOperationAuthority): Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
/** Expected endpoint authority for a guarded host operation, outside encrypted protocol data. */
|
|
20
|
+
export interface SyncOperationAuthority {
|
|
21
|
+
vaultId: string;
|
|
22
|
+
devicePublicKey: string;
|
|
23
|
+
epoch: number;
|
|
24
|
+
membershipGeneration: number;
|
|
25
|
+
}
|
|
26
|
+
/** Immutable sealed records addressed by opaque 64-character lowercase hex keys. */
|
|
27
|
+
export interface SealedSyncRecord {
|
|
28
|
+
epoch: number;
|
|
29
|
+
ciphertext: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
/** Optional repository extension for independently encrypted, indexed local records. */
|
|
32
|
+
export interface SyncIndexedRepository extends SyncRepository {
|
|
33
|
+
readonly records: {
|
|
34
|
+
get(key: string): Promise<SealedSyncRecord | null>;
|
|
35
|
+
/** False preserves the existing record unchanged, including concurrent insertion. */
|
|
36
|
+
putIfAbsent(key: string, record: SealedSyncRecord): Promise<boolean>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Captured by the owning host; a missing file is represented by null. */
|
|
40
|
+
export interface SyncFileGeneration {
|
|
41
|
+
fileId: string;
|
|
42
|
+
contentGeneration: number;
|
|
43
|
+
hostFileId?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SyncFileMutation {
|
|
46
|
+
id: string;
|
|
47
|
+
kind: "write" | "remove";
|
|
48
|
+
path: string;
|
|
49
|
+
expected: SyncFileGeneration | null;
|
|
50
|
+
fileId: string;
|
|
51
|
+
}
|
|
52
|
+
/** Capture only protocol-neutral mutation data; file handles may carry read functions. */
|
|
53
|
+
export declare function snapshotSyncFileMutation(input: SyncFileMutation): SyncFileMutation;
|
|
54
|
+
export interface SyncFileMutationReceipt {
|
|
55
|
+
operation: SyncFileMutation;
|
|
56
|
+
result: SyncFileGeneration | null;
|
|
57
|
+
}
|
|
58
|
+
/** The host journals immutable operation IDs and verifies guards at mutation. */
|
|
59
|
+
export interface SyncFileMutationAccess {
|
|
60
|
+
receipt(operationId: string, authority?: SyncOperationAuthority): Promise<SyncFileMutationReceipt | null>;
|
|
61
|
+
mutate(operation: SyncFileMutation, source?: ReadableStream<Uint8Array>, authority?: SyncOperationAuthority): Promise<SyncFileMutationReceipt>;
|
|
62
|
+
}
|
|
63
|
+
export interface SyncFileDescriptor extends SyncFileGeneration {
|
|
64
|
+
path: string;
|
|
65
|
+
}
|
|
66
|
+
export interface SyncGuardedFile extends SyncFileDescriptor {
|
|
67
|
+
openRead(): Promise<ReadableStream<Uint8Array>>;
|
|
68
|
+
}
|
|
69
|
+
/** Captured working files and durable encrypted state owned by the current host. */
|
|
70
|
+
export interface SyncLocalHostCapabilities {
|
|
71
|
+
list(): Promise<SyncGuardedFile[]>;
|
|
72
|
+
readonly repository: SyncRepository;
|
|
73
|
+
readonly apply: SyncFileMutationAccess;
|
|
74
|
+
}
|
|
75
|
+
/** Trusted host adapter. Neither this controller nor storage paths enter plugin facades. */
|
|
76
|
+
export interface SyncHostLocalProvider {
|
|
77
|
+
forRuntime(runtimeId: string): SyncLocalHostCapabilities;
|
|
78
|
+
revokeRuntime(runtimeId: string): void | Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
/** The owning host protects editor state for the entire callback, including recovery. */
|
|
81
|
+
export type SyncLocalApplyGuard = <T>(path: string | undefined, operation: () => Promise<T>) => Promise<T>;
|
|
82
|
+
/** Minimal opaque crypto boundary; implementations retain all secret material. */
|
|
83
|
+
export interface SyncVaultStateCrypto {
|
|
84
|
+
status(): Promise<{
|
|
85
|
+
state: "unenrolled" | "locked" | "unlocked";
|
|
86
|
+
suite: "lapis-sync-v1-xchacha20poly1305-schnorr";
|
|
87
|
+
vaultId?: string;
|
|
88
|
+
devicePublicKey?: string;
|
|
89
|
+
deviceState?: "active" | "revoked" | "recovery-pending";
|
|
90
|
+
currentEpoch?: number;
|
|
91
|
+
membershipGeneration?: number;
|
|
92
|
+
}>;
|
|
93
|
+
encryptObject(input: {
|
|
94
|
+
epoch: number;
|
|
95
|
+
type: "local-state";
|
|
96
|
+
plaintext: Uint8Array;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
bytes: Uint8Array;
|
|
99
|
+
objectHash: string;
|
|
100
|
+
}>;
|
|
101
|
+
decryptObject(input: {
|
|
102
|
+
epoch: number;
|
|
103
|
+
type: "local-state";
|
|
104
|
+
envelope: Uint8Array;
|
|
105
|
+
}): Promise<Uint8Array>;
|
|
106
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Capture only protocol-neutral mutation data; file handles may carry read functions. */
|
|
2
|
+
export function snapshotSyncFileMutation(input) {
|
|
3
|
+
return {
|
|
4
|
+
id: input.id,
|
|
5
|
+
kind: input.kind,
|
|
6
|
+
path: input.path,
|
|
7
|
+
fileId: input.fileId,
|
|
8
|
+
expected: input.expected === null
|
|
9
|
+
? null
|
|
10
|
+
: {
|
|
11
|
+
fileId: input.expected.fileId,
|
|
12
|
+
contentGeneration: input.expected.contentGeneration,
|
|
13
|
+
...(input.expected.hostFileId !== undefined
|
|
14
|
+
? { hostFileId: input.expected.hostFileId }
|
|
15
|
+
: {}),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
package/dist/view.svelte.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
-
import { DateTime } from "luxon";
|
|
3
2
|
import { Editor } from "./editor.svelte";
|
|
4
3
|
import { mount, unmount } from "svelte";
|
|
5
4
|
import EmptyViewComponent from "./components/empty-view/empty-view.svelte";
|
|
@@ -330,22 +329,34 @@ export class TextFileView extends FileView {
|
|
|
330
329
|
return this.app.vault.adapter;
|
|
331
330
|
}
|
|
332
331
|
async onLoadFile(file) {
|
|
333
|
-
|
|
332
|
+
const application = this.app;
|
|
333
|
+
return application.telemetry.measureAsync("view.text_file.on_load_file", async (span) => {
|
|
334
334
|
span.setAttribute("view.type", this.getViewType());
|
|
335
335
|
span.setAttribute("file.extension", file.extension);
|
|
336
|
-
const
|
|
336
|
+
const published = await this.editor.loadDocument(file, () => application.telemetry.measureAsync("vault.read", async () => application.vault.read(file), {
|
|
337
337
|
attributes: {
|
|
338
338
|
"file.extension": file.extension,
|
|
339
339
|
"view.type": this.getViewType(),
|
|
340
340
|
},
|
|
341
341
|
slowThresholdMs: 100,
|
|
342
|
+
}), (contents) => {
|
|
343
|
+
if (this.app !== application || this.editor.app !== application)
|
|
344
|
+
throw new Error("Editor document load authority changed");
|
|
345
|
+
const previousFile = this.file, previousData = this.data;
|
|
346
|
+
try {
|
|
347
|
+
this.file = file.copy();
|
|
348
|
+
this.editor.extensions = application.editorExtensions(file.extension, { ...this.getState(), file: file.path });
|
|
349
|
+
this.data = contents;
|
|
350
|
+
this.setViewData(contents, true);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
this.file = previousFile;
|
|
354
|
+
this.data = previousData;
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
342
357
|
});
|
|
343
|
-
this.file
|
|
344
|
-
|
|
345
|
-
await this.setState({ ...this.getState(), file: file.path });
|
|
346
|
-
this.editor.extensions = this.app.editorExtensions(this.file.extension, this.getState());
|
|
347
|
-
this.data = contents;
|
|
348
|
-
this.setViewData(contents, true);
|
|
358
|
+
if (published && this.editor.file === file)
|
|
359
|
+
await this.setState({ ...this.getState(), file: file.path });
|
|
349
360
|
}, {
|
|
350
361
|
attributes: {
|
|
351
362
|
"view.type": this.getViewType(),
|
|
@@ -364,18 +375,16 @@ export class TextFileView extends FileView {
|
|
|
364
375
|
return Promise.resolve();
|
|
365
376
|
}
|
|
366
377
|
onUnloadFile(file) {
|
|
378
|
+
this.editor.cancelDocumentLoad();
|
|
367
379
|
this.setState({ ...this.getState(), file: "" });
|
|
368
380
|
return Promise.resolve();
|
|
369
381
|
}
|
|
370
|
-
save(clear) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.clear();
|
|
378
|
-
}
|
|
379
|
-
});
|
|
382
|
+
async save(clear) {
|
|
383
|
+
const target = this.editor.file;
|
|
384
|
+
this.editor.save();
|
|
385
|
+
await this.editor.flushChanges();
|
|
386
|
+
this.data = this.editor.data;
|
|
387
|
+
if (clear && this.editor.file === target)
|
|
388
|
+
this.clear();
|
|
380
389
|
}
|
|
381
390
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapis-notes/api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Lapis Notes API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -224,6 +224,21 @@
|
|
|
224
224
|
"types": "./dist/build-tools/vite.d.ts",
|
|
225
225
|
"import": "./dist/build-tools/vite.js",
|
|
226
226
|
"default": "./dist/build-tools/vite.js"
|
|
227
|
+
},
|
|
228
|
+
"./sync-repository": {
|
|
229
|
+
"types": "./dist/sync-repository.d.ts",
|
|
230
|
+
"import": "./dist/sync-repository.js",
|
|
231
|
+
"default": "./dist/sync-repository.js"
|
|
232
|
+
},
|
|
233
|
+
"./sync-security-host": {
|
|
234
|
+
"types": "./dist/sync-host-security.d.ts",
|
|
235
|
+
"import": "./dist/sync-host-security.js",
|
|
236
|
+
"default": "./dist/sync-host-security.js"
|
|
237
|
+
},
|
|
238
|
+
"./metadata-store": {
|
|
239
|
+
"types": "./dist/metadata-store.d.ts",
|
|
240
|
+
"import": "./dist/metadata-store.js",
|
|
241
|
+
"default": "./dist/metadata-store.js"
|
|
227
242
|
}
|
|
228
243
|
},
|
|
229
244
|
"devDependencies": {
|
|
@@ -266,6 +281,7 @@
|
|
|
266
281
|
"check": "pnpm --dir ../.. check:no-tailwind packages/api/src/lib/components && pnpm check:svelte && pnpm check:types",
|
|
267
282
|
"check:svelte": "svelte-check --fail-on-warnings --output human",
|
|
268
283
|
"check:types": "tsc --noEmit -p tsconfig.types.json",
|
|
284
|
+
"check:pack-types": "tsc -p tsconfig.contracts.json",
|
|
269
285
|
"dev": "pnpm --dir ../.. storybook",
|
|
270
286
|
"storybook": "pnpm --dir ../.. storybook",
|
|
271
287
|
"test": "vitest run",
|