@mdbase-dev/connect 0.1.0-beta.110 → 0.1.0-beta.112
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/README.md +47 -0
- package/dist/advanced.d.ts +2 -0
- package/dist/base64.js +11 -3
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +4 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/connection-transport.d.ts +2 -0
- package/dist/connection-transport.js +24 -19
- package/dist/connection.d.ts +3 -0
- package/dist/connection.js +4 -0
- package/dist/crypto.js +3 -10
- package/dist/direct-access.js +13 -8
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/internal-types.d.ts +2 -0
- package/dist/local-file-transport.js +4 -4
- package/dist/operation-helpers.d.ts +1 -1
- package/dist/operation-helpers.js +7 -2
- package/dist/outcomes.d.ts +5 -5
- package/dist/outcomes.js +1 -0
- package/dist/pending-mutation-store.d.ts +3 -0
- package/dist/pending-mutation-store.js +10 -0
- package/dist/record-digest.d.ts +14 -0
- package/dist/record-digest.js +20 -0
- package/dist/record-session.d.ts +164 -0
- package/dist/record-session.js +449 -0
- package/dist/records.d.ts +56 -0
- package/dist/records.js +150 -0
- package/package.json +2 -2
package/dist/records.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { connectProblem } from "./errors.js";
|
|
2
|
+
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
3
|
+
import { MdbaseRecordSession } from "./record-session.js";
|
|
4
|
+
/**
|
|
5
|
+
* Editable record sessions for one connection: `connection.records`. Every
|
|
6
|
+
* view that opens the same path shares one session, so a record never has
|
|
7
|
+
* two competing writers in one application.
|
|
8
|
+
*/
|
|
9
|
+
export class MdbaseRecords {
|
|
10
|
+
connection;
|
|
11
|
+
pendingUpdate;
|
|
12
|
+
entries = new Map();
|
|
13
|
+
opening = new Map();
|
|
14
|
+
constructor(connection,
|
|
15
|
+
/** Finds this record's interrupted update, which survives reloads in durable storage. */
|
|
16
|
+
pendingUpdate = async () => null) {
|
|
17
|
+
this.connection = connection;
|
|
18
|
+
this.pendingUpdate = pendingUpdate;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read a record and hold its editing session. Session options apply when
|
|
22
|
+
* the session is first created; later opens share it as it is. A save
|
|
23
|
+
* interrupted before a reload resumes as exact recovery.
|
|
24
|
+
*/
|
|
25
|
+
async open(path, options = {}) {
|
|
26
|
+
for (;;) {
|
|
27
|
+
const existing = this.entries.get(path);
|
|
28
|
+
if (existing)
|
|
29
|
+
return connectSuccess(this.lease(existing));
|
|
30
|
+
const shared = this.opening.get(path);
|
|
31
|
+
const opening = shared ?? this.load(path, options);
|
|
32
|
+
if (!shared) {
|
|
33
|
+
this.opening.set(path, opening);
|
|
34
|
+
void opening.then(() => this.opening.delete(path));
|
|
35
|
+
}
|
|
36
|
+
const opened = await opening;
|
|
37
|
+
if (opened.ok)
|
|
38
|
+
return connectSuccess(this.lease(opened.value));
|
|
39
|
+
// Another caller's cancellation or deadline does not end this one's open.
|
|
40
|
+
const foreign = shared && (opened.problem.code === "operation_cancelled" || opened.problem.code === "timeout");
|
|
41
|
+
if (!foreign || options.signal?.aborted)
|
|
42
|
+
return opened;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Keep open sessions current from a collection watch: changed records are
|
|
47
|
+
* refreshed, renamed records are followed and deleted records are marked.
|
|
48
|
+
* Returns a function that stops following.
|
|
49
|
+
*/
|
|
50
|
+
follow(watch) {
|
|
51
|
+
return watch.subscribe((change) => this.apply(change), (status) => {
|
|
52
|
+
// Changes were missed: every open record may be stale.
|
|
53
|
+
if (status.state === "reset_required") {
|
|
54
|
+
for (const entry of this.entries.values())
|
|
55
|
+
void entry.session.refresh();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async load(path, options) {
|
|
60
|
+
const { autosave, ...request } = options;
|
|
61
|
+
const read = await this.connection.read({ path }, request);
|
|
62
|
+
if (!read.ok)
|
|
63
|
+
return read;
|
|
64
|
+
const existing = this.entries.get(read.value.path);
|
|
65
|
+
if (existing)
|
|
66
|
+
return connectSuccess(existing);
|
|
67
|
+
const entry = { path: read.value.path, leases: 0 };
|
|
68
|
+
entry.session = new MdbaseRecordSession(read.value, this.adapter(entry), { autosave });
|
|
69
|
+
const interrupted = await this.pendingUpdate(entry.path);
|
|
70
|
+
if (interrupted)
|
|
71
|
+
entry.session.resumeRecovery(interrupted);
|
|
72
|
+
this.entries.set(entry.path, entry);
|
|
73
|
+
return connectSuccess(entry);
|
|
74
|
+
}
|
|
75
|
+
/** Writes and reads follow the entry's current path, so a followed rename needs no new session. */
|
|
76
|
+
adapter(entry) {
|
|
77
|
+
return {
|
|
78
|
+
revision: (record) => record.revision,
|
|
79
|
+
body: (record) => record.body ?? "",
|
|
80
|
+
frontmatter: (record) => record.frontmatter,
|
|
81
|
+
write: (base, change, options) => this.connection.update({
|
|
82
|
+
path: entry.path,
|
|
83
|
+
ifRevision: base.revision,
|
|
84
|
+
patch: (change.patch ?? {}),
|
|
85
|
+
...(change.body === undefined ? {} : { body: change.body })
|
|
86
|
+
}, options),
|
|
87
|
+
read: (_base, options) => this.connection.read({ path: entry.path }, options),
|
|
88
|
+
recover: async (requestId, options) => {
|
|
89
|
+
const pending = this.connection.pendingMutation(requestId);
|
|
90
|
+
return pending
|
|
91
|
+
? pending.recover(options)
|
|
92
|
+
: connectFailure(connectProblem("no_pending_mutation", "The interrupted write is no longer pending."));
|
|
93
|
+
},
|
|
94
|
+
isPending: (requestId) => this.connection.pendingMutation(requestId) !== null
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
lease(entry) {
|
|
98
|
+
entry.leases += 1;
|
|
99
|
+
entry.stopRetiring?.();
|
|
100
|
+
entry.stopRetiring = undefined;
|
|
101
|
+
let released = false;
|
|
102
|
+
return {
|
|
103
|
+
session: entry.session,
|
|
104
|
+
release: () => {
|
|
105
|
+
if (released)
|
|
106
|
+
return;
|
|
107
|
+
released = true;
|
|
108
|
+
entry.leases -= 1;
|
|
109
|
+
this.retire(entry);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Drop an unheld session once it has nothing left to save. */
|
|
114
|
+
retire(entry) {
|
|
115
|
+
if (entry.leases > 0)
|
|
116
|
+
return;
|
|
117
|
+
const drop = () => {
|
|
118
|
+
const { state } = entry.session.snapshot;
|
|
119
|
+
if (state !== "saved" && state !== "deleted")
|
|
120
|
+
return false;
|
|
121
|
+
entry.stopRetiring?.();
|
|
122
|
+
entry.stopRetiring = undefined;
|
|
123
|
+
if (this.entries.get(entry.path) === entry)
|
|
124
|
+
this.entries.delete(entry.path);
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
if (!drop())
|
|
128
|
+
entry.stopRetiring = entry.session.subscribe(() => void drop());
|
|
129
|
+
}
|
|
130
|
+
apply(change) {
|
|
131
|
+
const { payload } = change;
|
|
132
|
+
if (change.type === "mdbase.record.renamed") {
|
|
133
|
+
const entry = typeof payload.from === "string" ? this.entries.get(payload.from) : undefined;
|
|
134
|
+
if (!entry || typeof payload.to !== "string")
|
|
135
|
+
return;
|
|
136
|
+
this.entries.delete(entry.path);
|
|
137
|
+
entry.path = payload.to;
|
|
138
|
+
this.entries.set(entry.path, entry);
|
|
139
|
+
void entry.session.refresh();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (change.type !== "mdbase.record.modified" && change.type !== "mdbase.record.deleted")
|
|
143
|
+
return;
|
|
144
|
+
const entry = typeof payload.path === "string" ? this.entries.get(payload.path) : undefined;
|
|
145
|
+
// The echo of this session's own acknowledged write needs no read.
|
|
146
|
+
if (!entry || payload.revision === entry.session.snapshot.record.revision)
|
|
147
|
+
return;
|
|
148
|
+
void entry.session.refresh();
|
|
149
|
+
}
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mdbase-dev/connect",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.112",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@mdbase-dev/connect-protocol": "0.1.0-beta.
|
|
36
|
+
"@mdbase-dev/connect-protocol": "0.1.0-beta.112"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"esbuild": "^0.28.1",
|