@feltdb/core 0.6.14 → 0.7.1
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 +36 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +116 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +19 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +290 -0
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +471 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +72 -3
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +931 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +125 -11
- package/dist/db.d.ts +2 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +29 -2
- package/dist/embedded-transaction.d.ts +9 -0
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +101 -3
- package/dist/feltdb.d.ts +16 -0
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +1 -0
- package/dist/http-db.d.ts +11 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +16 -2
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +5 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +1 -0
- package/dist/studio-app/assets/index-sQyf4Ewl.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +127 -9
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +87 -3
- package/package.json +1 -1
- package/dist/studio-app/assets/index-D4RZ44qs.js +0 -28
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#17211b"/><title>FeltDB Studio</title> <script type="module" crossorigin src="/assets/index-
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#17211b"/><title>FeltDB Studio</title> <script type="module" crossorigin src="/assets/index-sQyf4Ewl.js"></script>
|
|
2
2
|
<link rel="stylesheet" crossorigin href="/assets/index-C1GWyazR.css">
|
|
3
3
|
</head><body><div id="root"></div></body></html>
|
package/dist/transaction.d.ts
CHANGED
|
@@ -22,6 +22,50 @@
|
|
|
22
22
|
* a runtime that cannot provide that path refuses the transaction rather than
|
|
23
23
|
* silently degrading it into a sequence of independent writes.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* What must be true of one record for the transaction to commit.
|
|
27
|
+
*
|
|
28
|
+
* Evaluated by the authority inside the same atomic boundary as the writes.
|
|
29
|
+
* A client-side check cannot provide this: between the check and the write,
|
|
30
|
+
* another writer commits.
|
|
31
|
+
*
|
|
32
|
+
* Every predicate is optional and only a supplied one is checked. `version` is
|
|
33
|
+
* the record's `__version` -- the field you read back and pass to
|
|
34
|
+
* `updateIfVersion` -- and it is owned by the writer, so a transaction that
|
|
35
|
+
* wants to exclude its own successor must write an advanced `__version`.
|
|
36
|
+
*/
|
|
37
|
+
export interface AtomicTransactionPrecondition {
|
|
38
|
+
collection: string;
|
|
39
|
+
id: string;
|
|
40
|
+
/** The record must not exist. */
|
|
41
|
+
requireAbsent?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The record's current `__version`, spelled as `Collection.updateIfVersion`
|
|
44
|
+
* spells it.
|
|
45
|
+
*
|
|
46
|
+
* `ifVersion` and `expectedVersion` are the same predicate; `ifVersion` is
|
|
47
|
+
* the public spelling and `expectedVersion` is what goes on the wire.
|
|
48
|
+
* Supplying both with different values is an error rather than a silent
|
|
49
|
+
* preference, because a caller who wrote two different numbers did not mean
|
|
50
|
+
* either of them.
|
|
51
|
+
*/
|
|
52
|
+
ifVersion?: number;
|
|
53
|
+
/** The record's current `__version`. Wire spelling of `ifVersion`. */
|
|
54
|
+
expectedVersion?: number;
|
|
55
|
+
/** The record's authority epoch. */
|
|
56
|
+
expectedEpoch?: number;
|
|
57
|
+
/** The id of an unexpired lease the caller believes it holds. */
|
|
58
|
+
expectedLeaseId?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Predicates that may be attached to a staged write. */
|
|
61
|
+
export interface AtomicTransactionGuard {
|
|
62
|
+
requireAbsent?: boolean;
|
|
63
|
+
/** Public spelling of `expectedVersion`; the two are one predicate. */
|
|
64
|
+
ifVersion?: number;
|
|
65
|
+
expectedVersion?: number;
|
|
66
|
+
expectedEpoch?: number;
|
|
67
|
+
expectedLeaseId?: string;
|
|
68
|
+
}
|
|
25
69
|
/** One staged operation. An absent value means delete. */
|
|
26
70
|
export interface AtomicTransactionOperationRequest {
|
|
27
71
|
collection: string;
|
|
@@ -29,10 +73,47 @@ export interface AtomicTransactionOperationRequest {
|
|
|
29
73
|
value?: Record<string, unknown>;
|
|
30
74
|
/** When set, the record must not already exist for the transaction to commit. */
|
|
31
75
|
requireAbsent?: boolean;
|
|
76
|
+
/** Public spelling of `expectedVersion`; the two are one predicate. */
|
|
77
|
+
ifVersion?: number;
|
|
78
|
+
/** Fence this write on the record's current `__version`. */
|
|
79
|
+
expectedVersion?: number;
|
|
80
|
+
expectedEpoch?: number;
|
|
81
|
+
expectedLeaseId?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A transaction described in full, rather than staged through a callback.
|
|
85
|
+
*
|
|
86
|
+
* The same request the builder produces, taking the same path to the same
|
|
87
|
+
* authority call. It exists because a caller that already knows every operation
|
|
88
|
+
* — because it read state, decided, and is now committing that decision — has
|
|
89
|
+
* nothing to stage.
|
|
90
|
+
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* await db.transaction({
|
|
93
|
+
* transactionId,
|
|
94
|
+
* preconditions: [{ collection: 'sessions', id, ifVersion: version }],
|
|
95
|
+
* operations: [
|
|
96
|
+
* { collection: 'sessions', id, value: nextSession },
|
|
97
|
+
* { collection: 'changes', id: changeId, value: change },
|
|
98
|
+
* ],
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export interface AtomicTransactionDocument {
|
|
103
|
+
transactionId?: string;
|
|
104
|
+
preconditions?: AtomicTransactionPrecondition[];
|
|
105
|
+
operations: AtomicTransactionOperationRequest[];
|
|
32
106
|
}
|
|
33
107
|
export interface AtomicTransactionRequest {
|
|
34
108
|
transactionId: string;
|
|
35
109
|
operations: AtomicTransactionOperationRequest[];
|
|
110
|
+
/**
|
|
111
|
+
* Conditions on records the transaction need not write. Read/decide/write
|
|
112
|
+
* fencing routinely guards on a record it does not modify -- a session
|
|
113
|
+
* generation, an ownership epoch -- and expressing that as a no-op write
|
|
114
|
+
* would make the guard a mutation.
|
|
115
|
+
*/
|
|
116
|
+
preconditions?: AtomicTransactionPrecondition[];
|
|
36
117
|
}
|
|
37
118
|
export interface AtomicTransactionResult {
|
|
38
119
|
/** Durable identity of the transaction. Stable across restart and replay. */
|
|
@@ -46,9 +127,7 @@ export interface AtomicTransactionResult {
|
|
|
46
127
|
/** Staging surface for one collection inside a transaction. */
|
|
47
128
|
export interface AtomicTransactionCollection<T = unknown> {
|
|
48
129
|
/** Stage a write. */
|
|
49
|
-
set(id: string | number, value: T, options?:
|
|
50
|
-
requireAbsent?: boolean;
|
|
51
|
-
}): AtomicTransactionCollection<T>;
|
|
130
|
+
set(id: string | number, value: T, options?: AtomicTransactionGuard): AtomicTransactionCollection<T>;
|
|
52
131
|
/**
|
|
53
132
|
* Stage a write, in the argument order used by `Collection.insert`.
|
|
54
133
|
*
|
|
@@ -56,21 +135,27 @@ export interface AtomicTransactionCollection<T = unknown> {
|
|
|
56
135
|
* rest of the collection API does not have to change shape inside a
|
|
57
136
|
* transaction.
|
|
58
137
|
*/
|
|
59
|
-
insert(value: T, id: string | number, options?:
|
|
60
|
-
requireAbsent?: boolean;
|
|
61
|
-
}): AtomicTransactionCollection<T>;
|
|
138
|
+
insert(value: T, id: string | number, options?: AtomicTransactionGuard): AtomicTransactionCollection<T>;
|
|
62
139
|
/** Stage a delete. */
|
|
63
|
-
delete(id: string | number, options?:
|
|
64
|
-
requireAbsent?: boolean;
|
|
65
|
-
}): AtomicTransactionCollection<T>;
|
|
140
|
+
delete(id: string | number, options?: AtomicTransactionGuard): AtomicTransactionCollection<T>;
|
|
66
141
|
}
|
|
67
142
|
/** The handle passed to a transaction callback. */
|
|
68
143
|
export interface AtomicTransactionScope {
|
|
69
144
|
/** The transaction's durable identity, available while staging. */
|
|
70
145
|
readonly transactionId: string;
|
|
71
146
|
collection<T = unknown>(name: string): AtomicTransactionCollection<T>;
|
|
147
|
+
/**
|
|
148
|
+
* Require something of a record without writing it.
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* tx.require({ collection: 'sessions', id: 's1', expectedVersion: 12 });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
require(condition: AtomicTransactionPrecondition): AtomicTransactionScope;
|
|
72
155
|
/** Operations staged so far. */
|
|
73
156
|
readonly operations: readonly AtomicTransactionOperationRequest[];
|
|
157
|
+
/** Preconditions staged so far. */
|
|
158
|
+
readonly preconditions: readonly AtomicTransactionPrecondition[];
|
|
74
159
|
}
|
|
75
160
|
export interface AtomicTransactionOptions {
|
|
76
161
|
/**
|
|
@@ -84,14 +169,47 @@ export declare class AtomicTransactionError extends Error {
|
|
|
84
169
|
readonly transactionId: string;
|
|
85
170
|
constructor(message: string, transactionId: string);
|
|
86
171
|
}
|
|
172
|
+
/** Which predicate failed, and what the authority actually holds. */
|
|
173
|
+
export interface ConditionalConflictDetail {
|
|
174
|
+
predicate: 'version' | 'epoch' | 'lease' | 'present' | 'missing';
|
|
175
|
+
collection: string;
|
|
176
|
+
key: string;
|
|
177
|
+
expected?: number | string;
|
|
178
|
+
actual?: number | string | null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A transaction was refused because a precondition did not hold.
|
|
182
|
+
*
|
|
183
|
+
* Distinct from every other failure a transaction can have, and carried as a
|
|
184
|
+
* type rather than a message: a conflict means re-read and decide again, while
|
|
185
|
+
* an authentication, network, validation or server error means something else
|
|
186
|
+
* entirely, and a caller must not have to parse prose to tell them apart.
|
|
187
|
+
*
|
|
188
|
+
* A conflict guarantees that **zero** operations committed.
|
|
189
|
+
*/
|
|
190
|
+
export declare class ConditionalConflictError extends AtomicTransactionError {
|
|
191
|
+
readonly code: "PRECONDITION_FAILED";
|
|
192
|
+
readonly failure?: ConditionalConflictDetail;
|
|
193
|
+
constructor(message: string, transactionId: string, failure?: ConditionalConflictDetail);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Normalise a precondition to the single wire field.
|
|
197
|
+
*
|
|
198
|
+
* `ifVersion` is the public spelling and `expectedVersion` is the wire's. They
|
|
199
|
+
* are one predicate, so two different values for it is a caller error.
|
|
200
|
+
*/
|
|
201
|
+
export declare function normalisePrecondition(condition: AtomicTransactionPrecondition): AtomicTransactionPrecondition;
|
|
87
202
|
/** Collects operations without performing any I/O. */
|
|
88
203
|
export declare class AtomicTransactionBuilder implements AtomicTransactionScope {
|
|
89
204
|
readonly transactionId: string;
|
|
90
205
|
private readonly staged;
|
|
206
|
+
private readonly required;
|
|
91
207
|
private sealed;
|
|
92
208
|
constructor(transactionId?: string);
|
|
93
209
|
get operations(): readonly AtomicTransactionOperationRequest[];
|
|
94
210
|
collection<T = unknown>(name: string): AtomicTransactionCollection<T>;
|
|
211
|
+
get preconditions(): readonly AtomicTransactionPrecondition[];
|
|
212
|
+
require(condition: AtomicTransactionPrecondition): AtomicTransactionScope;
|
|
95
213
|
/** Freeze the builder and produce the request to send. */
|
|
96
214
|
seal(): AtomicTransactionRequest;
|
|
97
215
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,0DAA0D;AAC1D,MAAM,WAAW,iCAAiC;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,iFAAiF;IACjF,aAAa,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
|
package/dist/transaction.js
CHANGED
|
@@ -31,6 +31,40 @@ export class AtomicTransactionError extends Error {
|
|
|
31
31
|
this.transactionId = transactionId;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A transaction was refused because a precondition did not hold.
|
|
36
|
+
*
|
|
37
|
+
* Distinct from every other failure a transaction can have, and carried as a
|
|
38
|
+
* type rather than a message: a conflict means re-read and decide again, while
|
|
39
|
+
* an authentication, network, validation or server error means something else
|
|
40
|
+
* entirely, and a caller must not have to parse prose to tell them apart.
|
|
41
|
+
*
|
|
42
|
+
* A conflict guarantees that **zero** operations committed.
|
|
43
|
+
*/
|
|
44
|
+
export class ConditionalConflictError extends AtomicTransactionError {
|
|
45
|
+
code = 'PRECONDITION_FAILED';
|
|
46
|
+
failure;
|
|
47
|
+
constructor(message, transactionId, failure) {
|
|
48
|
+
super(message, transactionId);
|
|
49
|
+
this.name = 'ConditionalConflictError';
|
|
50
|
+
this.failure = failure;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Normalise a precondition to the single wire field.
|
|
55
|
+
*
|
|
56
|
+
* `ifVersion` is the public spelling and `expectedVersion` is the wire's. They
|
|
57
|
+
* are one predicate, so two different values for it is a caller error.
|
|
58
|
+
*/
|
|
59
|
+
export function normalisePrecondition(condition) {
|
|
60
|
+
const { ifVersion, expectedVersion, ...rest } = condition;
|
|
61
|
+
if (ifVersion !== undefined && expectedVersion !== undefined && ifVersion !== expectedVersion) {
|
|
62
|
+
throw new Error(`precondition on ${condition.collection}:${condition.id} supplies ifVersion `
|
|
63
|
+
+ `${ifVersion} and expectedVersion ${expectedVersion}; they are the same predicate`);
|
|
64
|
+
}
|
|
65
|
+
const version = ifVersion ?? expectedVersion;
|
|
66
|
+
return version === undefined ? { ...rest } : { ...rest, expectedVersion: version };
|
|
67
|
+
}
|
|
34
68
|
function generateTransactionId() {
|
|
35
69
|
const random = Math.random().toString(36).slice(2, 10);
|
|
36
70
|
return `tx_${Date.now().toString(36)}_${random}`;
|
|
@@ -39,6 +73,7 @@ function generateTransactionId() {
|
|
|
39
73
|
export class AtomicTransactionBuilder {
|
|
40
74
|
transactionId;
|
|
41
75
|
staged = [];
|
|
76
|
+
required = [];
|
|
42
77
|
sealed = false;
|
|
43
78
|
constructor(transactionId) {
|
|
44
79
|
this.transactionId = transactionId || generateTransactionId();
|
|
@@ -49,7 +84,17 @@ export class AtomicTransactionBuilder {
|
|
|
49
84
|
collection(name) {
|
|
50
85
|
if (!name || !name.trim())
|
|
51
86
|
throw new Error('collection name is required');
|
|
52
|
-
const stage = (
|
|
87
|
+
const stage = (staged) => {
|
|
88
|
+
// Normalised here so that an `ifVersion` written on an operation cannot
|
|
89
|
+
// be silently ignored — a declared-but-dropped predicate is the defect
|
|
90
|
+
// this whole capability exists to remove.
|
|
91
|
+
const { ifVersion, expectedVersion, ...rest } = staged;
|
|
92
|
+
if (ifVersion !== undefined && expectedVersion !== undefined && ifVersion !== expectedVersion) {
|
|
93
|
+
throw new Error(`${staged.collection}:${staged.id} supplies ifVersion ${ifVersion} and expectedVersion `
|
|
94
|
+
+ `${expectedVersion}; they are the same predicate`);
|
|
95
|
+
}
|
|
96
|
+
const version = ifVersion ?? expectedVersion;
|
|
97
|
+
const operation = version === undefined ? { ...rest } : { ...rest, expectedVersion: version };
|
|
53
98
|
if (this.sealed) {
|
|
54
99
|
throw new Error('this transaction has already been committed; start a new one');
|
|
55
100
|
}
|
|
@@ -69,20 +114,59 @@ export class AtomicTransactionBuilder {
|
|
|
69
114
|
id: String(id),
|
|
70
115
|
value: value,
|
|
71
116
|
requireAbsent: options?.requireAbsent,
|
|
117
|
+
ifVersion: options?.ifVersion,
|
|
118
|
+
expectedVersion: options?.expectedVersion,
|
|
119
|
+
expectedEpoch: options?.expectedEpoch,
|
|
120
|
+
expectedLeaseId: options?.expectedLeaseId,
|
|
72
121
|
});
|
|
73
122
|
return surface;
|
|
74
123
|
},
|
|
75
124
|
insert: (value, id, options) => surface.set(id, value, options),
|
|
76
125
|
delete: (id, options) => {
|
|
77
|
-
stage({
|
|
126
|
+
stage({
|
|
127
|
+
collection: name,
|
|
128
|
+
id: String(id),
|
|
129
|
+
requireAbsent: options?.requireAbsent,
|
|
130
|
+
ifVersion: options?.ifVersion,
|
|
131
|
+
expectedVersion: options?.expectedVersion,
|
|
132
|
+
expectedEpoch: options?.expectedEpoch,
|
|
133
|
+
expectedLeaseId: options?.expectedLeaseId,
|
|
134
|
+
});
|
|
78
135
|
return surface;
|
|
79
136
|
},
|
|
80
137
|
};
|
|
81
138
|
return surface;
|
|
82
139
|
}
|
|
140
|
+
get preconditions() {
|
|
141
|
+
return this.required;
|
|
142
|
+
}
|
|
143
|
+
require(condition) {
|
|
144
|
+
if (this.sealed) {
|
|
145
|
+
throw new Error('this transaction has already been committed; start a new one');
|
|
146
|
+
}
|
|
147
|
+
if (!condition.collection?.trim() || !String(condition.id ?? '').trim()) {
|
|
148
|
+
throw new Error('a precondition requires a collection and an id');
|
|
149
|
+
}
|
|
150
|
+
const normalised = normalisePrecondition(condition);
|
|
151
|
+
const constrains = normalised.requireAbsent === true
|
|
152
|
+
|| normalised.expectedVersion !== undefined
|
|
153
|
+
|| normalised.expectedEpoch !== undefined
|
|
154
|
+
|| normalised.expectedLeaseId !== undefined;
|
|
155
|
+
if (!constrains) {
|
|
156
|
+
// Refused rather than ignored: a precondition that constrains nothing
|
|
157
|
+
// reads as protection and provides none.
|
|
158
|
+
throw new Error(`precondition on ${condition.collection}:${condition.id} constrains nothing`);
|
|
159
|
+
}
|
|
160
|
+
this.required.push({ ...normalised, id: String(condition.id) });
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
83
163
|
/** Freeze the builder and produce the request to send. */
|
|
84
164
|
seal() {
|
|
85
165
|
this.sealed = true;
|
|
86
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
transactionId: this.transactionId,
|
|
168
|
+
operations: [...this.staged],
|
|
169
|
+
preconditions: [...this.required],
|
|
170
|
+
};
|
|
87
171
|
}
|
|
88
172
|
}
|
package/package.json
CHANGED