@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.3
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/SKILL.md +304 -320
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
- package/dist/engine-wasm/wasm/lix_engine.js +9 -13
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +103 -14
- package/dist/open-lix.js +3 -0
- package/dist/sqlite/index.js +99 -22
- package/dist-engine-src/README.md +18 -0
- package/dist-engine-src/src/backend/kv.rs +358 -0
- package/dist-engine-src/src/backend/mod.rs +12 -0
- package/dist-engine-src/src/backend/testing.rs +658 -0
- package/dist-engine-src/src/backend/types.rs +96 -0
- package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
- package/dist-engine-src/src/binary_cas/codec.rs +346 -0
- package/dist-engine-src/src/binary_cas/context.rs +139 -0
- package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
- package/dist-engine-src/src/binary_cas/mod.rs +11 -0
- package/dist-engine-src/src/binary_cas/types.rs +121 -0
- package/dist-engine-src/src/catalog/context.rs +412 -0
- package/dist-engine-src/src/catalog/mod.rs +10 -0
- package/dist-engine-src/src/catalog/schema.rs +4 -0
- package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
- package/dist-engine-src/src/cel/context.rs +86 -0
- package/dist-engine-src/src/cel/error.rs +19 -0
- package/dist-engine-src/src/cel/mod.rs +8 -0
- package/dist-engine-src/src/cel/provider.rs +9 -0
- package/dist-engine-src/src/cel/runtime.rs +167 -0
- package/dist-engine-src/src/cel/value.rs +50 -0
- package/dist-engine-src/src/commit_graph/context.rs +901 -0
- package/dist-engine-src/src/commit_graph/mod.rs +11 -0
- package/dist-engine-src/src/commit_graph/types.rs +109 -0
- package/dist-engine-src/src/commit_graph/walker.rs +756 -0
- package/dist-engine-src/src/commit_store/codec.rs +887 -0
- package/dist-engine-src/src/commit_store/context.rs +944 -0
- package/dist-engine-src/src/commit_store/materialization.rs +84 -0
- package/dist-engine-src/src/commit_store/mod.rs +16 -0
- package/dist-engine-src/src/commit_store/storage.rs +600 -0
- package/dist-engine-src/src/commit_store/types.rs +215 -0
- package/dist-engine-src/src/common/error.rs +313 -0
- package/dist-engine-src/src/common/fingerprint.rs +3 -0
- package/dist-engine-src/src/common/fs_path.rs +1336 -0
- package/dist-engine-src/src/common/identity.rs +145 -0
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +40 -0
- package/dist-engine-src/src/common/mod.rs +23 -0
- package/dist-engine-src/src/common/types.rs +105 -0
- package/dist-engine-src/src/common/wire.rs +222 -0
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +225 -0
- package/dist-engine-src/src/entity_identity.rs +405 -0
- package/dist-engine-src/src/functions/context.rs +292 -0
- package/dist-engine-src/src/functions/deterministic.rs +113 -0
- package/dist-engine-src/src/functions/mod.rs +18 -0
- package/dist-engine-src/src/functions/provider.rs +130 -0
- package/dist-engine-src/src/functions/state.rs +336 -0
- package/dist-engine-src/src/functions/types.rs +37 -0
- package/dist-engine-src/src/init.rs +558 -0
- package/dist-engine-src/src/json_store/compression.rs +77 -0
- package/dist-engine-src/src/json_store/context.rs +423 -0
- package/dist-engine-src/src/json_store/encoded.rs +15 -0
- package/dist-engine-src/src/json_store/mod.rs +12 -0
- package/dist-engine-src/src/json_store/store.rs +1109 -0
- package/dist-engine-src/src/json_store/types.rs +217 -0
- package/dist-engine-src/src/lib.rs +62 -0
- package/dist-engine-src/src/live_state/context.rs +2019 -0
- package/dist-engine-src/src/live_state/mod.rs +15 -0
- package/dist-engine-src/src/live_state/overlay.rs +75 -0
- package/dist-engine-src/src/live_state/reader.rs +23 -0
- package/dist-engine-src/src/live_state/types.rs +222 -0
- package/dist-engine-src/src/live_state/visibility.rs +223 -0
- package/dist-engine-src/src/plugin/archive.rs +438 -0
- package/dist-engine-src/src/plugin/component.rs +183 -0
- package/dist-engine-src/src/plugin/install.rs +619 -0
- package/dist-engine-src/src/plugin/manifest.rs +516 -0
- package/dist-engine-src/src/plugin/materializer.rs +477 -0
- package/dist-engine-src/src/plugin/mod.rs +33 -0
- package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
- package/dist-engine-src/src/plugin/storage.rs +74 -0
- package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
- package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
- package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
- package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
- package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
- package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +187 -0
- package/dist-engine-src/src/schema/definition.rs +742 -0
- package/dist-engine-src/src/schema/key.rs +138 -0
- package/dist-engine-src/src/schema/mod.rs +20 -0
- package/dist-engine-src/src/schema/seed.rs +14 -0
- package/dist-engine-src/src/schema/tests.rs +780 -0
- package/dist-engine-src/src/session/context.rs +364 -0
- package/dist-engine-src/src/session/create_version.rs +88 -0
- package/dist-engine-src/src/session/execute.rs +478 -0
- package/dist-engine-src/src/session/merge/analysis.rs +102 -0
- package/dist-engine-src/src/session/merge/apply.rs +23 -0
- package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
- package/dist-engine-src/src/session/merge/mod.rs +11 -0
- package/dist-engine-src/src/session/merge/stats.rs +65 -0
- package/dist-engine-src/src/session/merge/version.rs +427 -0
- package/dist-engine-src/src/session/mod.rs +27 -0
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +109 -0
- package/dist-engine-src/src/sql2/change_provider.rs +331 -0
- package/dist-engine-src/src/sql2/classify.rs +182 -0
- package/dist-engine-src/src/sql2/context.rs +311 -0
- package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
- package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
- package/dist-engine-src/src/sql2/dml.rs +148 -0
- package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
- package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
- package/dist-engine-src/src/sql2/error.rs +216 -0
- package/dist-engine-src/src/sql2/execute.rs +3440 -0
- package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
- package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
- package/dist-engine-src/src/sql2/history_projection.rs +56 -0
- package/dist-engine-src/src/sql2/history_provider.rs +412 -0
- package/dist-engine-src/src/sql2/history_route.rs +657 -0
- package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
- package/dist-engine-src/src/sql2/mod.rs +46 -0
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
- package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
- package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +63 -0
- package/dist-engine-src/src/sql2/record_batch.rs +17 -0
- package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
- package/dist-engine-src/src/sql2/runtime.rs +60 -0
- package/dist-engine-src/src/sql2/session.rs +132 -0
- package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
- package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
- package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
- package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
- package/dist-engine-src/src/sql2/version_scope.rs +394 -0
- package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
- package/dist-engine-src/src/storage/context.rs +356 -0
- package/dist-engine-src/src/storage/mod.rs +14 -0
- package/dist-engine-src/src/storage/read_scope.rs +88 -0
- package/dist-engine-src/src/storage/types.rs +501 -0
- package/dist-engine-src/src/storage_bench.rs +4863 -0
- package/dist-engine-src/src/test_support.rs +228 -0
- package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
- package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
- package/dist-engine-src/src/tracked_state/context.rs +1867 -0
- package/dist-engine-src/src/tracked_state/diff.rs +686 -0
- package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +492 -0
- package/dist-engine-src/src/tracked_state/mod.rs +32 -0
- package/dist-engine-src/src/tracked_state/storage.rs +375 -0
- package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
- package/dist-engine-src/src/tracked_state/types.rs +231 -0
- package/dist-engine-src/src/transaction/commit.rs +1484 -0
- package/dist-engine-src/src/transaction/context.rs +1548 -0
- package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
- package/dist-engine-src/src/transaction/mod.rs +13 -0
- package/dist-engine-src/src/transaction/normalization.rs +890 -0
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
- package/dist-engine-src/src/transaction/staging.rs +1731 -0
- package/dist-engine-src/src/transaction/types.rs +460 -0
- package/dist-engine-src/src/transaction/validation.rs +5830 -0
- package/dist-engine-src/src/untracked_state/codec.rs +307 -0
- package/dist-engine-src/src/untracked_state/context.rs +98 -0
- package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
- package/dist-engine-src/src/untracked_state/mod.rs +15 -0
- package/dist-engine-src/src/untracked_state/storage.rs +396 -0
- package/dist-engine-src/src/untracked_state/types.rs +146 -0
- package/dist-engine-src/src/version/context.rs +40 -0
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +13 -0
- package/dist-engine-src/src/version/refs.rs +330 -0
- package/dist-engine-src/src/version/stage_rows.rs +67 -0
- package/dist-engine-src/src/version/types.rs +21 -0
- package/dist-engine-src/src/wasm/mod.rs +60 -0
- package/package.json +68 -64
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
use crate::functions::FunctionProvider;
|
|
2
|
+
|
|
3
|
+
const DETERMINISTIC_UUID_COUNTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
|
|
4
|
+
|
|
5
|
+
/// Deterministic function provider for engine execution.
|
|
6
|
+
///
|
|
7
|
+
/// The provider is pure runtime state: it does not load or persist the sequence
|
|
8
|
+
/// itself. Session/transaction code owns that boundary so tests can decide when
|
|
9
|
+
/// deterministic state is read and written.
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub(crate) struct DeterministicFunctionProvider {
|
|
12
|
+
next_sequence: i64,
|
|
13
|
+
timestamp_shuffle: bool,
|
|
14
|
+
highest_seen: Option<i64>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl DeterministicFunctionProvider {
|
|
18
|
+
pub(crate) fn new(next_sequence: i64, timestamp_shuffle: bool) -> Self {
|
|
19
|
+
Self {
|
|
20
|
+
next_sequence,
|
|
21
|
+
timestamp_shuffle,
|
|
22
|
+
highest_seen: None,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(crate) fn highest_seen(&self) -> Option<i64> {
|
|
27
|
+
self.highest_seen
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn take_sequence(&mut self) -> i64 {
|
|
31
|
+
let current = self.next_sequence;
|
|
32
|
+
self.next_sequence += 1;
|
|
33
|
+
self.highest_seen = Some(current);
|
|
34
|
+
current
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl FunctionProvider for DeterministicFunctionProvider {
|
|
39
|
+
fn uuid_v7(&mut self) -> String {
|
|
40
|
+
let counter = self.take_sequence();
|
|
41
|
+
let counter_bits = (counter as u64) & DETERMINISTIC_UUID_COUNTER_MASK;
|
|
42
|
+
format!("01920000-0000-7000-8000-{counter_bits:012x}")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn timestamp(&mut self) -> String {
|
|
46
|
+
let counter = self.take_sequence();
|
|
47
|
+
let millis = if self.timestamp_shuffle {
|
|
48
|
+
shuffled_timestamp_millis(counter)
|
|
49
|
+
} else {
|
|
50
|
+
counter
|
|
51
|
+
};
|
|
52
|
+
let dt = chrono::DateTime::<chrono::Utc>::from_timestamp_millis(millis)
|
|
53
|
+
.unwrap_or(chrono::DateTime::<chrono::Utc>::UNIX_EPOCH);
|
|
54
|
+
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn deterministic_sequence_persist_highest_seen(&self) -> Option<i64> {
|
|
58
|
+
self.highest_seen()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn shuffled_timestamp_millis(counter: i64) -> i64 {
|
|
63
|
+
const WINDOW: i64 = 1000;
|
|
64
|
+
const MULTIPLIER: i64 = 733;
|
|
65
|
+
const OFFSET: i64 = 271;
|
|
66
|
+
|
|
67
|
+
let cycle = counter.div_euclid(WINDOW);
|
|
68
|
+
let within = counter.rem_euclid(WINDOW);
|
|
69
|
+
let shuffled = (within * MULTIPLIER + OFFSET).rem_euclid(WINDOW);
|
|
70
|
+
cycle * WINDOW + shuffled
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[cfg(test)]
|
|
74
|
+
mod tests {
|
|
75
|
+
use super::*;
|
|
76
|
+
use crate::functions::DeterministicSequence;
|
|
77
|
+
|
|
78
|
+
#[test]
|
|
79
|
+
fn deterministic_uuid_uses_sequence_counter() {
|
|
80
|
+
let mut provider = DeterministicFunctionProvider::new(0, false);
|
|
81
|
+
|
|
82
|
+
assert_eq!(provider.uuid_v7(), "01920000-0000-7000-8000-000000000000");
|
|
83
|
+
assert_eq!(provider.uuid_v7(), "01920000-0000-7000-8000-000000000001");
|
|
84
|
+
assert_eq!(provider.highest_seen(), Some(1));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn deterministic_timestamp_uses_sequence_counter() {
|
|
89
|
+
let mut provider = DeterministicFunctionProvider::new(1, false);
|
|
90
|
+
|
|
91
|
+
assert_eq!(provider.timestamp(), "1970-01-01T00:00:00.001Z");
|
|
92
|
+
assert_eq!(provider.highest_seen(), Some(1));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn deterministic_timestamp_shuffle_can_be_non_monotonic() {
|
|
97
|
+
let mut provider = DeterministicFunctionProvider::new(0, true);
|
|
98
|
+
let first = provider.timestamp();
|
|
99
|
+
let second = provider.timestamp();
|
|
100
|
+
|
|
101
|
+
assert!(second < first);
|
|
102
|
+
assert_eq!(provider.highest_seen(), Some(1));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn deterministic_sequence_can_start_after_persisted_highest_seen() {
|
|
107
|
+
let sequence = DeterministicSequence { highest_seen: 41 };
|
|
108
|
+
let mut provider = DeterministicFunctionProvider::new(sequence.next_sequence(), false);
|
|
109
|
+
|
|
110
|
+
assert_eq!(provider.uuid_v7(), "01920000-0000-7000-8000-00000000002a");
|
|
111
|
+
assert_eq!(provider.highest_seen(), Some(42));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//! Engine runtime function boundary.
|
|
2
|
+
//!
|
|
3
|
+
//! Sessions prepare one function context per execution. SQL, providers, and
|
|
4
|
+
//! transaction staging receive only a function provider; deterministic mode is
|
|
5
|
+
//! resolved privately inside this module.
|
|
6
|
+
|
|
7
|
+
mod context;
|
|
8
|
+
mod deterministic;
|
|
9
|
+
mod provider;
|
|
10
|
+
mod state;
|
|
11
|
+
mod types;
|
|
12
|
+
|
|
13
|
+
pub(crate) use context::FunctionContext;
|
|
14
|
+
pub(crate) use deterministic::DeterministicFunctionProvider;
|
|
15
|
+
pub(crate) use provider::{
|
|
16
|
+
FunctionProvider, FunctionProviderHandle, SharedFunctionProvider, SystemFunctionProvider,
|
|
17
|
+
};
|
|
18
|
+
pub(crate) use types::{DeterministicMode, DeterministicSequence};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
use std::sync::{Arc, Mutex};
|
|
2
|
+
|
|
3
|
+
use crate::cel::CelFunctionProvider;
|
|
4
|
+
|
|
5
|
+
/// Engine-owned runtime function provider trait.
|
|
6
|
+
pub(crate) trait FunctionProvider: Send {
|
|
7
|
+
fn uuid_v7(&mut self) -> String;
|
|
8
|
+
fn timestamp(&mut self) -> String;
|
|
9
|
+
|
|
10
|
+
fn deterministic_sequence_persist_highest_seen(&self) -> Option<i64> {
|
|
11
|
+
None
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub(crate) type FunctionProviderHandle = SharedFunctionProvider<Box<dyn FunctionProvider + Send>>;
|
|
16
|
+
|
|
17
|
+
/// Shareable function provider used across SQL planning, UDFs, and staging.
|
|
18
|
+
pub(crate) struct SharedFunctionProvider<P> {
|
|
19
|
+
inner: Arc<Mutex<P>>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl<P> Clone for SharedFunctionProvider<P> {
|
|
23
|
+
fn clone(&self) -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
inner: Arc::clone(&self.inner),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl<P> SharedFunctionProvider<P> {
|
|
31
|
+
pub(crate) fn new(provider: P) -> Self {
|
|
32
|
+
Self {
|
|
33
|
+
inner: Arc::new(Mutex::new(provider)),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn with_lock<R>(&self, f: impl FnOnce(&P) -> R) -> R {
|
|
38
|
+
let guard = self
|
|
39
|
+
.inner
|
|
40
|
+
.lock()
|
|
41
|
+
.expect("engine function provider mutex poisoned");
|
|
42
|
+
f(&guard)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn with_lock_mut<R>(&self, f: impl FnOnce(&mut P) -> R) -> R {
|
|
46
|
+
let mut guard = self
|
|
47
|
+
.inner
|
|
48
|
+
.lock()
|
|
49
|
+
.expect("engine function provider mutex poisoned");
|
|
50
|
+
f(&mut guard)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl<P> SharedFunctionProvider<P>
|
|
55
|
+
where
|
|
56
|
+
P: FunctionProvider,
|
|
57
|
+
{
|
|
58
|
+
pub(crate) fn call_uuid_v7(&self) -> String {
|
|
59
|
+
self.with_lock_mut(|provider| provider.uuid_v7())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub(crate) fn call_timestamp(&self) -> String {
|
|
63
|
+
self.with_lock_mut(|provider| provider.timestamp())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub(crate) fn deterministic_sequence_persist_highest_seen(&self) -> Option<i64> {
|
|
67
|
+
self.with_lock(|provider| provider.deterministic_sequence_persist_highest_seen())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl<P> CelFunctionProvider for SharedFunctionProvider<P>
|
|
72
|
+
where
|
|
73
|
+
P: FunctionProvider + Send + 'static,
|
|
74
|
+
{
|
|
75
|
+
fn call_uuid_v7(&self) -> String {
|
|
76
|
+
SharedFunctionProvider::call_uuid_v7(self)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn call_timestamp(&self) -> String {
|
|
80
|
+
SharedFunctionProvider::call_timestamp(self)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl<P> FunctionProvider for SharedFunctionProvider<P>
|
|
85
|
+
where
|
|
86
|
+
P: FunctionProvider,
|
|
87
|
+
{
|
|
88
|
+
fn uuid_v7(&mut self) -> String {
|
|
89
|
+
self.call_uuid_v7()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn timestamp(&mut self) -> String {
|
|
93
|
+
self.call_timestamp()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn deterministic_sequence_persist_highest_seen(&self) -> Option<i64> {
|
|
97
|
+
SharedFunctionProvider::deterministic_sequence_persist_highest_seen(self)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl<T> FunctionProvider for Box<T>
|
|
102
|
+
where
|
|
103
|
+
T: FunctionProvider + ?Sized,
|
|
104
|
+
{
|
|
105
|
+
fn uuid_v7(&mut self) -> String {
|
|
106
|
+
(**self).uuid_v7()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn timestamp(&mut self) -> String {
|
|
110
|
+
(**self).timestamp()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn deterministic_sequence_persist_highest_seen(&self) -> Option<i64> {
|
|
114
|
+
(**self).deterministic_sequence_persist_highest_seen()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// System-backed engine function provider.
|
|
119
|
+
#[derive(Debug, Default, Clone, Copy)]
|
|
120
|
+
pub(crate) struct SystemFunctionProvider;
|
|
121
|
+
|
|
122
|
+
impl FunctionProvider for SystemFunctionProvider {
|
|
123
|
+
fn uuid_v7(&mut self) -> String {
|
|
124
|
+
uuid::Uuid::now_v7().to_string()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn timestamp(&mut self) -> String {
|
|
128
|
+
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
use serde_json::Value as JsonValue;
|
|
2
|
+
use std::sync::Arc;
|
|
3
|
+
|
|
4
|
+
use crate::entity_identity::EntityIdentity;
|
|
5
|
+
use crate::functions::{DeterministicMode, DeterministicSequence};
|
|
6
|
+
use crate::json_store::NormalizedJson;
|
|
7
|
+
use crate::live_state::{LiveStateReader, LiveStateRowRequest, MaterializedLiveStateRow};
|
|
8
|
+
use crate::storage::StorageWriteSet;
|
|
9
|
+
use crate::untracked_state::UntrackedStateContext;
|
|
10
|
+
use crate::untracked_state::UntrackedStateRow;
|
|
11
|
+
use crate::GLOBAL_VERSION_ID;
|
|
12
|
+
use crate::{LixError, NullableKeyFilter};
|
|
13
|
+
|
|
14
|
+
pub(crate) const DETERMINISTIC_MODE_KEY: &str = "lix_deterministic_mode";
|
|
15
|
+
pub(crate) const DETERMINISTIC_SEQUENCE_KEY: &str = "lix_deterministic_sequence_number";
|
|
16
|
+
|
|
17
|
+
const KEY_VALUE_SCHEMA_KEY: &str = "lix_key_value";
|
|
18
|
+
|
|
19
|
+
/// Loads deterministic-mode settings from visible live state.
|
|
20
|
+
///
|
|
21
|
+
/// Missing mode means deterministic execution is disabled. Malformed mode rows
|
|
22
|
+
/// are errors because they would make runtime function behavior ambiguous.
|
|
23
|
+
pub(crate) async fn load_mode(
|
|
24
|
+
live_state: &dyn LiveStateReader,
|
|
25
|
+
) -> Result<DeterministicMode, LixError> {
|
|
26
|
+
let Some(row) = load_key_value_row(live_state, DETERMINISTIC_MODE_KEY).await? else {
|
|
27
|
+
return Ok(DeterministicMode::disabled());
|
|
28
|
+
};
|
|
29
|
+
let value = key_value_payload(&row, DETERMINISTIC_MODE_KEY)?;
|
|
30
|
+
parse_mode_value(value)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Loads the persisted deterministic sequence position.
|
|
34
|
+
///
|
|
35
|
+
/// Missing sequence means no deterministic values have been produced yet, so
|
|
36
|
+
/// execution starts at sequence zero.
|
|
37
|
+
pub(crate) async fn load_sequence(
|
|
38
|
+
live_state: &dyn LiveStateReader,
|
|
39
|
+
) -> Result<DeterministicSequence, LixError> {
|
|
40
|
+
let Some(row) = load_key_value_row(live_state, DETERMINISTIC_SEQUENCE_KEY).await? else {
|
|
41
|
+
return Ok(DeterministicSequence::uninitialized());
|
|
42
|
+
};
|
|
43
|
+
let value = key_value_payload(&row, DETERMINISTIC_SEQUENCE_KEY)?;
|
|
44
|
+
parse_sequence_value(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Persists the highest deterministic sequence value used by an execution.
|
|
48
|
+
///
|
|
49
|
+
/// The row is untracked global `lix_key_value` state: it is durable local
|
|
50
|
+
/// runtime state, not a changelog fact.
|
|
51
|
+
pub(crate) async fn stage_sequence(
|
|
52
|
+
writes: &mut StorageWriteSet,
|
|
53
|
+
sequence: DeterministicSequence,
|
|
54
|
+
timestamp: &str,
|
|
55
|
+
) -> Result<(), LixError> {
|
|
56
|
+
let snapshot_content = serde_json::to_string(&serde_json::json!({
|
|
57
|
+
"key": DETERMINISTIC_SEQUENCE_KEY,
|
|
58
|
+
"value": sequence.highest_seen,
|
|
59
|
+
}))
|
|
60
|
+
.map_err(|error| {
|
|
61
|
+
LixError::new(
|
|
62
|
+
"LIX_ERROR_UNKNOWN",
|
|
63
|
+
format!("deterministic sequence snapshot serialization failed: {error}"),
|
|
64
|
+
)
|
|
65
|
+
})?;
|
|
66
|
+
let snapshot = NormalizedJson::from_arc_unchecked(Arc::from(snapshot_content.as_str()));
|
|
67
|
+
let row =
|
|
68
|
+
deterministic_key_value_row(DETERMINISTIC_SEQUENCE_KEY, snapshot.as_str(), timestamp)?;
|
|
69
|
+
UntrackedStateContext::new()
|
|
70
|
+
.writer(writes)
|
|
71
|
+
.stage_rows(std::iter::once(row.as_ref()))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async fn load_key_value_row(
|
|
75
|
+
live_state: &dyn LiveStateReader,
|
|
76
|
+
key: &str,
|
|
77
|
+
) -> Result<Option<MaterializedLiveStateRow>, LixError> {
|
|
78
|
+
live_state
|
|
79
|
+
.load_row(&LiveStateRowRequest {
|
|
80
|
+
schema_key: KEY_VALUE_SCHEMA_KEY.to_string(),
|
|
81
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
82
|
+
entity_id: EntityIdentity::single(key),
|
|
83
|
+
file_id: NullableKeyFilter::Null,
|
|
84
|
+
})
|
|
85
|
+
.await
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn key_value_payload(row: &MaterializedLiveStateRow, key: &str) -> Result<JsonValue, LixError> {
|
|
89
|
+
let snapshot_content = row.snapshot_content.as_deref().ok_or_else(|| {
|
|
90
|
+
LixError::new(
|
|
91
|
+
"LIX_ERROR_UNKNOWN",
|
|
92
|
+
format!("deterministic key-value row '{key}' is missing snapshot_content"),
|
|
93
|
+
)
|
|
94
|
+
})?;
|
|
95
|
+
let snapshot = serde_json::from_str::<JsonValue>(snapshot_content).map_err(|error| {
|
|
96
|
+
LixError::new(
|
|
97
|
+
"LIX_ERROR_UNKNOWN",
|
|
98
|
+
format!("deterministic key-value row '{key}' has invalid JSON: {error}"),
|
|
99
|
+
)
|
|
100
|
+
})?;
|
|
101
|
+
let stored_key = snapshot.get("key").and_then(JsonValue::as_str);
|
|
102
|
+
if stored_key != Some(key) {
|
|
103
|
+
return Err(LixError::new(
|
|
104
|
+
"LIX_ERROR_UNKNOWN",
|
|
105
|
+
format!("deterministic key-value row '{key}' has mismatched key field"),
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
snapshot.get("value").cloned().ok_or_else(|| {
|
|
109
|
+
LixError::new(
|
|
110
|
+
"LIX_ERROR_UNKNOWN",
|
|
111
|
+
format!("deterministic key-value row '{key}' is missing value"),
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn parse_mode_value(value: JsonValue) -> Result<DeterministicMode, LixError> {
|
|
117
|
+
let Some(object) = value.as_object() else {
|
|
118
|
+
return Err(LixError::new(
|
|
119
|
+
"LIX_ERROR_UNKNOWN",
|
|
120
|
+
"deterministic mode value must be an object",
|
|
121
|
+
));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let enabled = object
|
|
125
|
+
.get("enabled")
|
|
126
|
+
.and_then(JsonValue::as_bool)
|
|
127
|
+
.unwrap_or(false);
|
|
128
|
+
if !enabled {
|
|
129
|
+
return Ok(DeterministicMode::disabled());
|
|
130
|
+
}
|
|
131
|
+
let timestamp_shuffle = object
|
|
132
|
+
.get("timestamp_shuffle")
|
|
133
|
+
.and_then(JsonValue::as_bool)
|
|
134
|
+
.unwrap_or(false);
|
|
135
|
+
Ok(DeterministicMode {
|
|
136
|
+
enabled,
|
|
137
|
+
timestamp_shuffle,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn parse_sequence_value(value: JsonValue) -> Result<DeterministicSequence, LixError> {
|
|
142
|
+
let Some(highest_seen) = value.as_i64() else {
|
|
143
|
+
return Err(LixError::new(
|
|
144
|
+
"LIX_ERROR_UNKNOWN",
|
|
145
|
+
"deterministic sequence value must be an integer",
|
|
146
|
+
));
|
|
147
|
+
};
|
|
148
|
+
Ok(DeterministicSequence { highest_seen })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn deterministic_key_value_row(
|
|
152
|
+
key: &str,
|
|
153
|
+
snapshot_content: &str,
|
|
154
|
+
timestamp: &str,
|
|
155
|
+
) -> Result<UntrackedStateRow, LixError> {
|
|
156
|
+
Ok(UntrackedStateRow {
|
|
157
|
+
entity_id: crate::entity_identity::EntityIdentity::single(key),
|
|
158
|
+
schema_key: KEY_VALUE_SCHEMA_KEY.to_string(),
|
|
159
|
+
file_id: None,
|
|
160
|
+
snapshot_content: Some(snapshot_content.to_string()),
|
|
161
|
+
metadata: None,
|
|
162
|
+
created_at: timestamp.to_string(),
|
|
163
|
+
updated_at: timestamp.to_string(),
|
|
164
|
+
global: true,
|
|
165
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[cfg(test)]
|
|
170
|
+
mod tests {
|
|
171
|
+
use std::sync::Arc;
|
|
172
|
+
|
|
173
|
+
use crate::backend::testing::UnitTestBackend;
|
|
174
|
+
use crate::live_state::{LiveStateContext, LiveStateRowRequest};
|
|
175
|
+
use crate::storage::StorageContext;
|
|
176
|
+
|
|
177
|
+
use super::*;
|
|
178
|
+
|
|
179
|
+
fn live_state_context() -> LiveStateContext {
|
|
180
|
+
LiveStateContext::new(
|
|
181
|
+
crate::tracked_state::TrackedStateContext::new(),
|
|
182
|
+
crate::untracked_state::UntrackedStateContext::new(),
|
|
183
|
+
crate::commit_graph::CommitGraphContext::new(),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[tokio::test]
|
|
188
|
+
async fn missing_mode_is_disabled() {
|
|
189
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
190
|
+
let storage = StorageContext::new(backend.clone());
|
|
191
|
+
let live_state = live_state_context();
|
|
192
|
+
let reader = live_state.reader(storage.clone());
|
|
193
|
+
|
|
194
|
+
let mode = load_mode(&reader)
|
|
195
|
+
.await
|
|
196
|
+
.expect("missing mode should decode");
|
|
197
|
+
|
|
198
|
+
assert_eq!(mode, DeterministicMode::disabled());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[tokio::test]
|
|
202
|
+
async fn valid_mode_decodes_flags() {
|
|
203
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
204
|
+
let storage = StorageContext::new(backend.clone());
|
|
205
|
+
let live_state = live_state_context();
|
|
206
|
+
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
207
|
+
write_test_key_value(
|
|
208
|
+
storage.clone(),
|
|
209
|
+
DETERMINISTIC_MODE_KEY,
|
|
210
|
+
serde_json::json!({
|
|
211
|
+
"enabled": true,
|
|
212
|
+
"timestamp_shuffle": true,
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
.await;
|
|
216
|
+
|
|
217
|
+
let reader = live_state.reader(storage.clone());
|
|
218
|
+
let mode = load_mode(&reader).await.expect("valid mode should decode");
|
|
219
|
+
|
|
220
|
+
assert_eq!(
|
|
221
|
+
mode,
|
|
222
|
+
DeterministicMode {
|
|
223
|
+
enabled: true,
|
|
224
|
+
timestamp_shuffle: true,
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[tokio::test]
|
|
230
|
+
async fn missing_sequence_is_uninitialized() {
|
|
231
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
232
|
+
let storage = StorageContext::new(backend.clone());
|
|
233
|
+
let live_state = live_state_context();
|
|
234
|
+
let reader = live_state.reader(storage.clone());
|
|
235
|
+
|
|
236
|
+
let sequence = load_sequence(&reader)
|
|
237
|
+
.await
|
|
238
|
+
.expect("missing sequence should decode");
|
|
239
|
+
|
|
240
|
+
assert_eq!(sequence, DeterministicSequence::uninitialized());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[tokio::test]
|
|
244
|
+
async fn valid_sequence_decodes_highest_seen() {
|
|
245
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
246
|
+
let storage = StorageContext::new(backend.clone());
|
|
247
|
+
let live_state = live_state_context();
|
|
248
|
+
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
249
|
+
write_test_key_value(
|
|
250
|
+
storage.clone(),
|
|
251
|
+
DETERMINISTIC_SEQUENCE_KEY,
|
|
252
|
+
serde_json::json!(41),
|
|
253
|
+
)
|
|
254
|
+
.await;
|
|
255
|
+
|
|
256
|
+
let reader = live_state.reader(storage.clone());
|
|
257
|
+
let sequence = load_sequence(&reader)
|
|
258
|
+
.await
|
|
259
|
+
.expect("valid sequence should decode");
|
|
260
|
+
|
|
261
|
+
assert_eq!(sequence, DeterministicSequence { highest_seen: 41 });
|
|
262
|
+
assert_eq!(sequence.next_sequence(), 42);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[tokio::test]
|
|
266
|
+
async fn write_sequence_persists_untracked_global_key_value() {
|
|
267
|
+
let backend = Arc::new(UnitTestBackend::new());
|
|
268
|
+
let storage = StorageContext::new(backend.clone());
|
|
269
|
+
let live_state = live_state_context();
|
|
270
|
+
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
271
|
+
let mut tx = storage
|
|
272
|
+
.begin_write_transaction()
|
|
273
|
+
.await
|
|
274
|
+
.expect("transaction should open");
|
|
275
|
+
|
|
276
|
+
let mut writes = StorageWriteSet::new();
|
|
277
|
+
stage_sequence(
|
|
278
|
+
&mut writes,
|
|
279
|
+
DeterministicSequence { highest_seen: 7 },
|
|
280
|
+
"1970-01-01T00:00:00.000Z",
|
|
281
|
+
)
|
|
282
|
+
.await
|
|
283
|
+
.expect("sequence should stage");
|
|
284
|
+
writes
|
|
285
|
+
.apply(&mut tx.as_mut())
|
|
286
|
+
.await
|
|
287
|
+
.expect("sequence should apply");
|
|
288
|
+
tx.commit().await.expect("transaction should commit");
|
|
289
|
+
|
|
290
|
+
let reader = live_state.reader(storage.clone());
|
|
291
|
+
let row = reader
|
|
292
|
+
.load_row(&LiveStateRowRequest {
|
|
293
|
+
schema_key: KEY_VALUE_SCHEMA_KEY.to_string(),
|
|
294
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
295
|
+
entity_id: crate::entity_identity::EntityIdentity::single(
|
|
296
|
+
DETERMINISTIC_SEQUENCE_KEY,
|
|
297
|
+
),
|
|
298
|
+
file_id: NullableKeyFilter::Null,
|
|
299
|
+
})
|
|
300
|
+
.await
|
|
301
|
+
.expect("sequence row should load")
|
|
302
|
+
.expect("sequence row should exist");
|
|
303
|
+
assert!(row.untracked);
|
|
304
|
+
assert!(row.global);
|
|
305
|
+
assert_eq!(row.change_id, None);
|
|
306
|
+
assert_eq!(row.commit_id, None);
|
|
307
|
+
assert_eq!(
|
|
308
|
+
row.snapshot_content.as_deref(),
|
|
309
|
+
Some("{\"key\":\"lix_deterministic_sequence_number\",\"value\":7}")
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async fn write_test_key_value(storage: StorageContext, key: &str, value: JsonValue) {
|
|
314
|
+
let mut tx = storage
|
|
315
|
+
.begin_write_transaction()
|
|
316
|
+
.await
|
|
317
|
+
.expect("transaction should open");
|
|
318
|
+
let snapshot_content = serde_json::to_string(&serde_json::json!({
|
|
319
|
+
"key": key,
|
|
320
|
+
"value": value,
|
|
321
|
+
}))
|
|
322
|
+
.expect("snapshot should serialize");
|
|
323
|
+
let mut writes = StorageWriteSet::new();
|
|
324
|
+
let row = deterministic_key_value_row(key, &snapshot_content, "1970-01-01T00:00:00.000Z")
|
|
325
|
+
.expect("test key-value should canonicalize");
|
|
326
|
+
UntrackedStateContext::new()
|
|
327
|
+
.writer(&mut writes)
|
|
328
|
+
.stage_rows(std::iter::once(row.as_ref()))
|
|
329
|
+
.expect("test key-value should stage");
|
|
330
|
+
writes
|
|
331
|
+
.apply(&mut tx.as_mut())
|
|
332
|
+
.await
|
|
333
|
+
.expect("test key-value should apply");
|
|
334
|
+
tx.commit().await.expect("transaction should commit");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/// Decoded deterministic-mode setting.
|
|
2
|
+
///
|
|
3
|
+
/// Storage can decide where this setting lives. The type only describes the
|
|
4
|
+
/// behavior engine should apply while preparing runtime functions.
|
|
5
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
6
|
+
pub(crate) struct DeterministicMode {
|
|
7
|
+
pub(crate) enabled: bool,
|
|
8
|
+
pub(crate) timestamp_shuffle: bool,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl DeterministicMode {
|
|
12
|
+
pub(crate) fn disabled() -> Self {
|
|
13
|
+
Self {
|
|
14
|
+
enabled: false,
|
|
15
|
+
timestamp_shuffle: false,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Persisted deterministic sequence position.
|
|
21
|
+
///
|
|
22
|
+
/// `highest_seen` is the last sequence value returned by the runtime provider.
|
|
23
|
+
/// The next deterministic execution starts at `highest_seen + 1`.
|
|
24
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
25
|
+
pub(crate) struct DeterministicSequence {
|
|
26
|
+
pub(crate) highest_seen: i64,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl DeterministicSequence {
|
|
30
|
+
pub(crate) fn uninitialized() -> Self {
|
|
31
|
+
Self { highest_seen: -1 }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub(crate) fn next_sequence(self) -> i64 {
|
|
35
|
+
self.highest_seen + 1
|
|
36
|
+
}
|
|
37
|
+
}
|