@photon-grove/evt 0.1.1 → 0.2.0

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.
Files changed (86) hide show
  1. package/README.md +51 -4
  2. package/dist/aggregate.d.ts +26 -0
  3. package/dist/aggregate.d.ts.map +1 -1
  4. package/dist/aggregate.js.map +1 -1
  5. package/dist/dynamodb/head.d.ts +6 -0
  6. package/dist/dynamodb/head.d.ts.map +1 -1
  7. package/dist/dynamodb/head.js +11 -0
  8. package/dist/dynamodb/head.js.map +1 -1
  9. package/dist/dynamodb/index.d.ts +2 -1
  10. package/dist/dynamodb/index.d.ts.map +1 -1
  11. package/dist/dynamodb/index.js +2 -1
  12. package/dist/dynamodb/index.js.map +1 -1
  13. package/dist/dynamodb/repository.d.ts +9 -2
  14. package/dist/dynamodb/repository.d.ts.map +1 -1
  15. package/dist/dynamodb/repository.js +123 -27
  16. package/dist/dynamodb/repository.js.map +1 -1
  17. package/dist/dynamodb/requests.d.ts +37 -3
  18. package/dist/dynamodb/requests.d.ts.map +1 -1
  19. package/dist/dynamodb/requests.js +67 -9
  20. package/dist/dynamodb/requests.js.map +1 -1
  21. package/dist/dynamodb/views.d.ts +18 -0
  22. package/dist/dynamodb/views.d.ts.map +1 -0
  23. package/dist/dynamodb/views.js +56 -0
  24. package/dist/dynamodb/views.js.map +1 -0
  25. package/dist/errors.d.ts +51 -4
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/errors.js +60 -4
  28. package/dist/errors.js.map +1 -1
  29. package/dist/index.d.ts +4 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/memory/repository.d.ts +3 -2
  34. package/dist/memory/repository.d.ts.map +1 -1
  35. package/dist/memory/repository.js +78 -29
  36. package/dist/memory/repository.js.map +1 -1
  37. package/dist/postgres/client.d.ts +32 -0
  38. package/dist/postgres/client.d.ts.map +1 -0
  39. package/dist/postgres/client.js +4 -0
  40. package/dist/postgres/client.js.map +1 -0
  41. package/dist/postgres/index.d.ts +4 -0
  42. package/dist/postgres/index.d.ts.map +1 -0
  43. package/dist/postgres/index.js +6 -0
  44. package/dist/postgres/index.js.map +1 -0
  45. package/dist/postgres/repository.d.ts +62 -0
  46. package/dist/postgres/repository.d.ts.map +1 -0
  47. package/dist/postgres/repository.js +598 -0
  48. package/dist/postgres/repository.js.map +1 -0
  49. package/dist/postgres/schema.d.ts +81 -0
  50. package/dist/postgres/schema.d.ts.map +1 -0
  51. package/dist/postgres/schema.js +249 -0
  52. package/dist/postgres/schema.js.map +1 -0
  53. package/dist/repository.d.ts +101 -6
  54. package/dist/repository.d.ts.map +1 -1
  55. package/dist/repository.js +108 -6
  56. package/dist/repository.js.map +1 -1
  57. package/dist/staging.d.ts +28 -0
  58. package/dist/staging.d.ts.map +1 -0
  59. package/dist/staging.js +89 -0
  60. package/dist/staging.js.map +1 -0
  61. package/dist/store.d.ts +15 -3
  62. package/dist/store.d.ts.map +1 -1
  63. package/dist/store.js +38 -54
  64. package/dist/store.js.map +1 -1
  65. package/dist/transaction.d.ts +113 -0
  66. package/dist/transaction.d.ts.map +1 -0
  67. package/dist/transaction.js +207 -0
  68. package/dist/transaction.js.map +1 -0
  69. package/package.json +5 -1
  70. package/src/aggregate.ts +28 -0
  71. package/src/dynamodb/head.ts +15 -0
  72. package/src/dynamodb/index.ts +2 -0
  73. package/src/dynamodb/repository.ts +177 -33
  74. package/src/dynamodb/requests.ts +114 -10
  75. package/src/dynamodb/views.ts +67 -0
  76. package/src/errors.ts +86 -4
  77. package/src/index.ts +19 -6
  78. package/src/memory/repository.ts +93 -32
  79. package/src/postgres/client.ts +37 -0
  80. package/src/postgres/index.ts +23 -0
  81. package/src/postgres/repository.ts +858 -0
  82. package/src/postgres/schema.ts +302 -0
  83. package/src/repository.ts +220 -8
  84. package/src/staging.ts +138 -0
  85. package/src/store.ts +66 -65
  86. package/src/transaction.ts +322 -0
@@ -0,0 +1,113 @@
1
+ import type { Aggregate, EventDefinitions, EventOf } from './aggregate.js';
2
+ import type { SerializedEvent } from './event.js';
3
+ import { type Metadata } from './metadata.js';
4
+ import { type CallOptions, type TransactionalRepository } from './repository.js';
5
+ import type { ExecuteOptions, Loaded } from './store.js';
6
+ declare const eventTypes: unique symbol;
7
+ /** What a transaction needs from its store. */
8
+ export interface TransactionContext {
9
+ readonly repository: TransactionalRepository;
10
+ load<S, C, D extends EventDefinitions>(aggregate: Aggregate<S, C, D>, entityId: string, options?: CallOptions): Promise<Loaded<S>>;
11
+ snapshotInterval(aggregate: {
12
+ readonly snapshotInterval?: number | undefined;
13
+ }): number;
14
+ /** The command's metadata with what the store fills in, and the commit ID stamped. */
15
+ metadata(partial?: Partial<Metadata>): Metadata;
16
+ }
17
+ /** A fence the caller stamps on a stream the transaction doesn't read. */
18
+ export interface CallerFence {
19
+ readonly entityId: string;
20
+ /** The stream's entity type, which a fence on a retained type is refused for. */
21
+ readonly entityType: string;
22
+ /** The head the caller observed: 0 for a stream with no events. */
23
+ readonly head: bigint;
24
+ }
25
+ /**
26
+ * What a transaction's commit did:
27
+ *
28
+ * - `committed`: every stream's events (possibly none) are stored, and every fence held. `events`
29
+ * are the stored events in the order the commit wrote them: by entity ID bytes, then sequence.
30
+ * - `duplicate`: an event a read stream folded in since its latest snapshot already carries the
31
+ * command ID, so nothing was written.
32
+ */
33
+ export type TransactionResult = {
34
+ kind: 'committed';
35
+ events: SerializedEvent[];
36
+ } | {
37
+ kind: 'duplicate';
38
+ commandId: string;
39
+ };
40
+ interface Stream {
41
+ readonly aggregate: Aggregate<any, any, any>;
42
+ readonly order: number;
43
+ readonly loaded: Loaded<unknown>;
44
+ state: unknown;
45
+ events: SerializedEvent[];
46
+ }
47
+ /**
48
+ * A stream read into a {@link Transaction}, typed by its aggregate. Pass it to
49
+ * {@link Transaction.append} to stage events for it; a read never appended to is fenced at the
50
+ * head it was read at.
51
+ */
52
+ export declare class StreamRead<S, D extends EventDefinitions> {
53
+ #private;
54
+ readonly [eventTypes]?: D;
55
+ /** @internal Built by {@link Transaction.read}. */
56
+ constructor(stream: Stream);
57
+ get entityId(): string;
58
+ /** The state, with the events appended through this handle. */
59
+ get state(): S;
60
+ /** The sequence of the last event, with the events appended through this handle. */
61
+ get sequence(): bigint;
62
+ /** The stream as it was read. */
63
+ get loaded(): Loaded<S>;
64
+ }
65
+ /**
66
+ * One atomic commit across several streams. {@link read} loads streams of any aggregate types,
67
+ * {@link append} stages the events decided for them, {@link fence} adds a fence the caller stamped
68
+ * on a stream it didn't read, and {@link commit} writes everything in one repository transaction,
69
+ * or nothing. A stream read but never appended to adds a fence the engine filled at the head it
70
+ * was read at, so the commit fails with a retryable `FenceConflictError` once another writer
71
+ * moves it. Begin one with {@link Store.transaction}.
72
+ *
73
+ * Every staging call checks its input before it changes anything, so one that throws leaves the
74
+ * transaction as it was. A transaction commits once: after {@link commit}, whatever it returned
75
+ * or threw, every call throws `invalidCommit`. Begin a new transaction to try again.
76
+ */
77
+ export declare class Transaction {
78
+ #private;
79
+ /** @internal Begun by {@link Store.transaction}. */
80
+ constructor(context: TransactionContext);
81
+ /**
82
+ * Loads a stream into the read set, as {@link Store.load} does, and returns its typed handle.
83
+ * Reading a stream the transaction already read, or is reading, throws `invalidCommit`.
84
+ */
85
+ read<S, C, D extends EventDefinitions>(aggregate: Aggregate<S, C, D>, entityId: string, options?: CallOptions): Promise<StreamRead<S, D>>;
86
+ /**
87
+ * Stages events for a stream this transaction read. They're applied to the handle's state, and
88
+ * appending again continues the sequence. The commit stamps their metadata, and runs the
89
+ * aggregate's projectors over every event appended to the stream, with its final state. A
90
+ * handle from another transaction throws `invalidCommit`.
91
+ */
92
+ append<S, D extends EventDefinitions>(read: StreamRead<S, D>, events: readonly EventOf<D>[]): this;
93
+ /**
94
+ * Adds a fence the caller stamped, for a stream the transaction doesn't read: the commit fails
95
+ * with a `FenceConflictError` once another writer moves the stream past `head`, which isn't
96
+ * retryable, since only the caller knows what it read. A fence on a stream the transaction reads,
97
+ * before or after the read, is dropped at commit: the read's own fence, or its write, holds the
98
+ * stream instead. A fence in a reserved namespace, at a head outside [0, 2^63 - 1), or on a
99
+ * stream already fenced throws, with nothing added.
100
+ */
101
+ fence(fence: CallerFence): this;
102
+ /**
103
+ * Stamps one commit ID and the command's metadata on every appended event, and commits every
104
+ * stream with events, a fence on every stream read without any, and the caller's fences, in one
105
+ * atomic repository transaction. On failure nothing is written. A command ID that an event a
106
+ * read stream folded in since its latest snapshot already carries is a `duplicate`, and nothing
107
+ * is written. Projector operations follow the streams in the order they were read. A commit
108
+ * while a read is still loading throws `invalidCommit`, and leaves the transaction open.
109
+ */
110
+ commit(options?: ExecuteOptions): Promise<TransactionResult>;
111
+ }
112
+ export {};
113
+ //# sourceMappingURL=transaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,207 @@
1
+ // The Store's multi-stream transaction (ADR 0004 C1, C2): a read set of any aggregate types, the
2
+ // events decided for some of them, and one atomic commit that fences every stream it read but
3
+ // didn't write.
4
+ import { EvtError } from './errors.js';
5
+ import { emptyMetadata } from './metadata.js';
6
+ import { checkNotReserved, } from './repository.js';
7
+ import { commitSnapshot, copyMetadata, projectCommit, serializeEvents } from './staging.js';
8
+ import { compareUtf8 } from './utf8.js';
9
+ const MAX_INT64 = 2n ** 63n - 1n;
10
+ /**
11
+ * A stream read into a {@link Transaction}, typed by its aggregate. Pass it to
12
+ * {@link Transaction.append} to stage events for it; a read never appended to is fenced at the
13
+ * head it was read at.
14
+ */
15
+ export class StreamRead {
16
+ #stream;
17
+ /** @internal Built by {@link Transaction.read}. */
18
+ constructor(stream) {
19
+ this.#stream = stream;
20
+ }
21
+ get entityId() {
22
+ return this.#stream.loaded.entityId;
23
+ }
24
+ /** The state, with the events appended through this handle. */
25
+ get state() {
26
+ return this.#stream.state;
27
+ }
28
+ /** The sequence of the last event, with the events appended through this handle. */
29
+ get sequence() {
30
+ return this.#stream.loaded.sequence + BigInt(this.#stream.events.length);
31
+ }
32
+ /** The stream as it was read. */
33
+ get loaded() {
34
+ return this.#stream.loaded;
35
+ }
36
+ }
37
+ /**
38
+ * One atomic commit across several streams. {@link read} loads streams of any aggregate types,
39
+ * {@link append} stages the events decided for them, {@link fence} adds a fence the caller stamped
40
+ * on a stream it didn't read, and {@link commit} writes everything in one repository transaction,
41
+ * or nothing. A stream read but never appended to adds a fence the engine filled at the head it
42
+ * was read at, so the commit fails with a retryable `FenceConflictError` once another writer
43
+ * moves it. Begin one with {@link Store.transaction}.
44
+ *
45
+ * Every staging call checks its input before it changes anything, so one that throws leaves the
46
+ * transaction as it was. A transaction commits once: after {@link commit}, whatever it returned
47
+ * or threw, every call throws `invalidCommit`. Begin a new transaction to try again.
48
+ */
49
+ export class Transaction {
50
+ #context;
51
+ #streams = new Map();
52
+ // Every stream read or being read, by ID, with the position of its read call.
53
+ #ids = new Map();
54
+ #fences = new Map();
55
+ #reads = 0;
56
+ #pending = 0;
57
+ #done = false;
58
+ /** @internal Begun by {@link Store.transaction}. */
59
+ constructor(context) {
60
+ this.#context = context;
61
+ }
62
+ /**
63
+ * Loads a stream into the read set, as {@link Store.load} does, and returns its typed handle.
64
+ * Reading a stream the transaction already read, or is reading, throws `invalidCommit`.
65
+ */
66
+ async read(aggregate, entityId, options = {}) {
67
+ this.#checkOpen();
68
+ if (this.#ids.has(entityId)) {
69
+ throw invalidCommit(`duplicate read of ${entityId}`);
70
+ }
71
+ // The ID is held while the load runs, so a concurrent read of the same stream is refused too,
72
+ // and the read keeps the place it was called in, whenever its load finishes.
73
+ const order = this.#reads++;
74
+ this.#ids.set(entityId, order);
75
+ this.#pending++;
76
+ let loaded;
77
+ try {
78
+ loaded = await this.#context.load(aggregate, entityId, options);
79
+ }
80
+ catch (error) {
81
+ this.#ids.delete(entityId);
82
+ throw error;
83
+ }
84
+ finally {
85
+ this.#pending--;
86
+ }
87
+ const stream = {
88
+ aggregate,
89
+ order,
90
+ loaded,
91
+ state: loaded.state,
92
+ events: [],
93
+ };
94
+ const read = new StreamRead(stream);
95
+ this.#streams.set(read, stream);
96
+ return read;
97
+ }
98
+ /**
99
+ * Stages events for a stream this transaction read. They're applied to the handle's state, and
100
+ * appending again continues the sequence. The commit stamps their metadata, and runs the
101
+ * aggregate's projectors over every event appended to the stream, with its final state. A
102
+ * handle from another transaction throws `invalidCommit`.
103
+ */
104
+ append(read, events) {
105
+ this.#checkOpen();
106
+ const stream = this.#streams.get(read);
107
+ if (stream === undefined) {
108
+ throw invalidCommit(`stream ${read.entityId} was not read by this transaction`);
109
+ }
110
+ // Serialized now, so an event the aggregate can't encode fails here, with nothing staged.
111
+ const after = stream.loaded.sequence + BigInt(stream.events.length);
112
+ const staged = serializeEvents(stream.aggregate, stream.loaded.entityId, after, stream.state, events, emptyMetadata());
113
+ stream.events.push(...staged.events);
114
+ stream.state = staged.state;
115
+ return this;
116
+ }
117
+ /**
118
+ * Adds a fence the caller stamped, for a stream the transaction doesn't read: the commit fails
119
+ * with a `FenceConflictError` once another writer moves the stream past `head`, which isn't
120
+ * retryable, since only the caller knows what it read. A fence on a stream the transaction reads,
121
+ * before or after the read, is dropped at commit: the read's own fence, or its write, holds the
122
+ * stream instead. A fence in a reserved namespace, at a head outside [0, 2^63 - 1), or on a
123
+ * stream already fenced throws, with nothing added.
124
+ */
125
+ fence(fence) {
126
+ this.#checkOpen();
127
+ checkNotReserved(fence.entityId);
128
+ if (fence.head < 0n || fence.head >= MAX_INT64 || this.#fences.has(fence.entityId)) {
129
+ throw invalidCommit(`invalid or duplicate fence ${fence.entityId}`);
130
+ }
131
+ this.#fences.set(fence.entityId, { ...fence });
132
+ return this;
133
+ }
134
+ /**
135
+ * Stamps one commit ID and the command's metadata on every appended event, and commits every
136
+ * stream with events, a fence on every stream read without any, and the caller's fences, in one
137
+ * atomic repository transaction. On failure nothing is written. A command ID that an event a
138
+ * read stream folded in since its latest snapshot already carries is a `duplicate`, and nothing
139
+ * is written. Projector operations follow the streams in the order they were read. A commit
140
+ * while a read is still loading throws `invalidCommit`, and leaves the transaction open.
141
+ */
142
+ async commit(options = {}) {
143
+ this.#checkOpen();
144
+ if (this.#pending > 0) {
145
+ throw invalidCommit('a read is still loading; await it before the commit');
146
+ }
147
+ this.#done = true;
148
+ const metadata = this.#context.metadata(options.metadata);
149
+ const commandId = metadata.commandId;
150
+ const streams = [...this.#streams.values()];
151
+ streams.sort((a, b) => a.order - b.order);
152
+ if (commandId !== null &&
153
+ commandId !== '' &&
154
+ streams.some((stream) => stream.loaded.seenCommandIds.has(commandId))) {
155
+ return { kind: 'duplicate', commandId };
156
+ }
157
+ const commits = [];
158
+ const fences = [];
159
+ for (const stream of streams) {
160
+ const { aggregate, loaded } = stream;
161
+ if (stream.events.length === 0) {
162
+ fences.push({
163
+ entityId: loaded.entityId,
164
+ entityType: aggregate.type,
165
+ head: loaded.sequence,
166
+ provenance: 'engine',
167
+ });
168
+ continue;
169
+ }
170
+ for (const event of stream.events) {
171
+ event.metadata = copyMetadata(metadata);
172
+ }
173
+ const interval = this.#context.snapshotInterval(aggregate);
174
+ const appended = stream.events.length;
175
+ const snapshot = commitSnapshot(aggregate, loaded.entityId, interval, loaded, appended, stream.state);
176
+ commits.push({
177
+ entityId: loaded.entityId,
178
+ events: stream.events,
179
+ snapshot,
180
+ projections: projectCommit(aggregate, loaded.entityId, stream.state, stream.events, snapshot),
181
+ });
182
+ }
183
+ // A caller's fence on a stream the transaction read is superseded by the read's own.
184
+ for (const fence of this.#fences.values()) {
185
+ if (!this.#ids.has(fence.entityId)) {
186
+ fences.push({ ...fence, provenance: 'caller' });
187
+ }
188
+ }
189
+ await this.#context.repository.commitTransaction({ streams: commits, fences }, options);
190
+ // Sorted apart from what the repository was given, which stays in read order.
191
+ const written = [...commits];
192
+ written.sort((a, b) => compareUtf8(a.entityId, b.entityId));
193
+ return {
194
+ kind: 'committed',
195
+ events: written.flatMap((commit) => commit.events),
196
+ };
197
+ }
198
+ #checkOpen() {
199
+ if (this.#done) {
200
+ throw invalidCommit('the transaction has committed; begin a new one');
201
+ }
202
+ }
203
+ }
204
+ function invalidCommit(message) {
205
+ return new EvtError('invalidCommit', `invalid commit: ${message}`);
206
+ }
207
+ //# sourceMappingURL=transaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photon-grove/evt",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Event sourcing for TypeScript, wire-compatible with the Go and Rust evt implementations.",
5
5
  "keywords": [
6
6
  "event-sourcing",
@@ -38,6 +38,10 @@
38
38
  "types": "./dist/memory/index.d.ts",
39
39
  "default": "./dist/memory/index.js"
40
40
  },
41
+ "./postgres": {
42
+ "types": "./dist/postgres/index.d.ts",
43
+ "default": "./dist/postgres/index.js"
44
+ },
41
45
  "./testing": {
42
46
  "types": "./dist/testing/index.d.ts",
43
47
  "default": "./dist/testing/index.js"
package/src/aggregate.ts CHANGED
@@ -5,6 +5,7 @@ import type {Codec} from './codec.js'
5
5
  import {EvtError, ReplayError, messageOf} from './errors.js'
6
6
  import type {SerializedEvent} from './event.js'
7
7
  import {type JsonValue, parseJson, stringifyJson} from './json.js'
8
+ import type {ProjectorGroup} from './repository.js'
8
9
  import {utf8Decode, utf8Encode} from './utf8.js'
9
10
 
10
11
  /**
@@ -103,6 +104,33 @@ export interface AggregateDefinition<S, C, D extends EventDefinitions> {
103
104
  * snapshots.
104
105
  */
105
106
  readonly snapshotInterval?: number | undefined
107
+ /**
108
+ * The projectors whose operations every commit of this type writes in the same transaction as
109
+ * its events, in this order, such as the DynamoDB backend's `viewProjector`.
110
+ */
111
+ readonly projectors?: readonly Projector<S>[] | undefined
112
+ }
113
+
114
+ /** What a projector sees of a commit: its entity, the state after its events, and the events. */
115
+ export interface Projection<S> {
116
+ readonly entityType: string
117
+ readonly entityId: string
118
+ /** The state after the commit's events. */
119
+ readonly state: S
120
+ /** The state as the aggregate's state codec encodes it, as a snapshot stores it. */
121
+ readonly encodedState: Uint8Array
122
+ /** Copies of the commit's events, as they are stored. */
123
+ readonly events: readonly Readonly<SerializedEvent>[]
124
+ }
125
+
126
+ /**
127
+ * Builds the operations a commit writes beside its events, in the same transaction, such as a
128
+ * view of the entity's state. Its group names the backend that writes it. `project` returns null
129
+ * when the commit needs none. It runs only for a commit with events, again on every retry, and it
130
+ * must not fail or change the state.
131
+ */
132
+ export interface Projector<S> {
133
+ project(projection: Projection<S>): ProjectorGroup | null
106
134
  }
107
135
 
108
136
  /** A checked aggregate type. */
@@ -72,6 +72,21 @@ export function headUpdate(
72
72
  }
73
73
  }
74
74
 
75
+ /** The condition of a fence on a stream with no head: it still has none. */
76
+ export const FENCE_ABSENT_CONDITION = 'attribute_not_exists(head)'
77
+
78
+ /** The condition of a fence on a stream with a head: it still equals the one observed. */
79
+ export const FENCE_CONDITION = 'head = :head'
80
+
81
+ /** The `ConditionCheck` a transaction fences a stream it read at `head` with. */
82
+ export function headFence(entityId: string, head: bigint): HeadAction {
83
+ const check = {entityId, updateExpression: null, names: []}
84
+
85
+ return head === 0n
86
+ ? {...check, conditionExpression: FENCE_ABSENT_CONDITION, values: []}
87
+ : {...check, conditionExpression: FENCE_CONDITION, values: [[':head', head]]}
88
+ }
89
+
75
90
  /**
76
91
  * An action's size in the commit budget, by the rule for any update, delete or condition check:
77
92
  * its key attributes plus its expression strings, name placeholders (the placeholder and the
@@ -37,9 +37,11 @@ export {
37
37
  } from './client.js'
38
38
  export {FEATURES_PK, REBUILD_LOCK_PK, manifestFromItem, markerItem} from './marker.js'
39
39
  export {DynamoRepository, type DynamoRepositoryOptions} from './repository.js'
40
+ export {DYNAMODB_BACKEND, transactItemSize, viewProjector} from './views.js'
40
41
  export {
41
42
  type CommitTtl,
42
43
  type SnapshotTtl,
43
44
  commitTransaction,
44
45
  snapshotTransaction,
46
+ transactionItems,
45
47
  } from './requests.js'