dson/api/
array.rs

1// (c) Copyright 2025 Helsing GmbH. All rights reserved.
2use crate::{
3    CausalContext, CausalDotStore, ExtensionType, Identifier, MvReg, OrArray, OrMap,
4    crdts::{
5        TypeVariantValue, Value,
6        orarray::{Position, Uid},
7        snapshot::{self, ToValue},
8    },
9};
10use std::{convert::Infallible, fmt};
11
12/*
13/// insert(𝑖𝑑𝑥, 𝑜𝛿 𝑖 ) – given an index 𝑖𝑑𝑥 and a method 𝑜𝛿
14/// 𝑖 from
15/// the API of some CRDT of type 𝑉 , The method assigns a
16/// unique id 𝑢𝑖𝑑, assigns a stable position identifier 𝑝 such that
17/// the new element in the sorted array appears at index 𝑖𝑑𝑥,
18/// and invokes apply(𝑢𝑖𝑑, 𝑜𝛿
19/// 𝑖 , 𝑝).
20update(𝑖𝑑𝑥, 𝑜𝛿
21𝑖 ) – given an index 𝑖𝑑𝑥 and a method 𝑜𝛿
22𝑖 of
23some CRDT type 𝑉 , The method finds the 𝑢𝑖𝑑 corresponding
24to the element at index 𝑖𝑑𝑥, finds the position 𝑝, and invokes
25apply(𝑢𝑖𝑑, 𝑜𝛿
26𝑖 , 𝑝).
27move(𝑜𝑙𝑑_𝑖𝑑𝑥, 𝑛𝑒𝑤_𝑖𝑑𝑥) – given two indexes, finds the ele-
28ment 𝑢𝑖𝑑 corresponding to the element at index 𝑜𝑙𝑑_𝑖𝑑𝑥,
29calculates the stable position identifier 𝑝 such that the el-
30ement in the sorted array will be at index 𝑛𝑒𝑤_𝑖𝑑𝑥, and
31invokes move(𝑢𝑖𝑑, 𝑝).
32delete(𝑖𝑑𝑥) – given an index 𝑖𝑑𝑥, finds the element 𝑢𝑖𝑑 corre-
33sponding to the element at index 𝑖𝑑𝑥, and invokes delete(𝑢𝑖𝑑).
34get(𝑖𝑑𝑥) – given an index 𝑖𝑑𝑥, finds the element 𝑢𝑖𝑑 corre-
35sponding to the element at index 𝑖𝑑𝑥, and invokes get(𝑢𝑖𝑑).
36*/
37
38/// Returns the values of this array without collapsing conflicts.
39pub fn values<C>(m: &OrArray<C>) -> snapshot::OrArray<snapshot::AllValues<'_, C::ValueRef<'_>>>
40where
41    C: ExtensionType,
42{
43    m.values()
44}
45
46/// Returns the values of this array assuming (and asserting) no conflicts on element values.
47// NOTE: A type alias won't help much here :melt:.
48#[allow(clippy::type_complexity)]
49pub fn value<C>(
50    m: &OrArray<C>,
51) -> Result<
52    snapshot::OrArray<snapshot::CollapsedValue<'_, C::ValueRef<'_>>>,
53    Box<snapshot::SingleValueError<<&OrArray<C> as ToValue>::LeafValue>>,
54>
55where
56    C: ExtensionType,
57{
58    m.value()
59}
60
61/// Creates a new array.
62pub fn create<C>() -> impl Fn(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
63where
64    C: ExtensionType + fmt::Debug + PartialEq,
65{
66    move |m, cc, id| m.create(cc, id)
67}
68
69/// Inserts a new element at the given index.
70pub fn insert<O, C>(
71    o: O,
72    idx: usize,
73) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
74where
75    O: FnOnce(&CausalContext, Identifier) -> CausalDotStore<Value<C>>,
76    C: ExtensionType + fmt::Debug + PartialEq,
77{
78    move |m, cc, id| {
79        let uid = cc.next_dot_for(id).into();
80        let p = create_position_for_index(m, idx);
81        m.insert(uid, o, p, cc, id)
82    }
83}
84
85/// Inserts a new map at the given index.
86pub fn insert_map<O, C>(
87    o: O,
88    idx: usize,
89) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
90where
91    O: FnOnce(&CausalContext, Identifier) -> CausalDotStore<OrMap<String, C>>,
92    C: ExtensionType + fmt::Debug + PartialEq,
93{
94    insert(move |cc, id| (o)(cc, id).map_store(Value::Map), idx)
95}
96
97/// Inserts a new array at the given index.
98pub fn insert_array<O, C>(
99    o: O,
100    idx: usize,
101) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
102where
103    O: FnOnce(&CausalContext, Identifier) -> CausalDotStore<OrArray<C>>,
104    C: ExtensionType + fmt::Debug + PartialEq,
105{
106    insert(move |cc, id| (o)(cc, id).map_store(Value::Array), idx)
107}
108
109/// Inserts a new register at the given index.
110pub fn insert_register<O, C>(
111    o: O,
112    idx: usize,
113) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
114where
115    O: FnOnce(&CausalContext, Identifier) -> CausalDotStore<MvReg>,
116    C: ExtensionType + fmt::Debug + PartialEq,
117{
118    insert(move |cc, id| (o)(cc, id).map_store(Value::Register), idx)
119}
120
121/// Applies a function to the element at the given index.
122pub fn apply<O, C>(
123    o: O,
124    idx: usize,
125) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
126where
127    O: FnOnce(&TypeVariantValue<C>, &CausalContext, Identifier) -> CausalDotStore<Value<C>>,
128    C: ExtensionType + fmt::Debug + PartialEq,
129{
130    move |m, cc, id| {
131        let uid = uid_from_index(m, idx);
132        assert_ne!(idx, m.len(), "index out of bounds");
133        let p = create_position_for_index(m, idx);
134        m.apply(uid, o, p, cc, id)
135    }
136}
137
138/// Applies a function to the map at the given index.
139pub fn apply_to_map<O, C>(
140    o: O,
141    idx: usize,
142) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
143where
144    O: FnOnce(&OrMap<String, C>, &CausalContext, Identifier) -> CausalDotStore<OrMap<String, C>>,
145    C: ExtensionType + fmt::Debug + PartialEq,
146{
147    apply(
148        move |m, cc, id| (o)(&m.map, cc, id).map_store(Value::Map),
149        idx,
150    )
151}
152
153/// Applies a function to the array at the given index.
154pub fn apply_to_array<O, C>(
155    o: O,
156    idx: usize,
157) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
158where
159    O: FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>,
160    C: ExtensionType + fmt::Debug + PartialEq,
161{
162    apply(
163        move |m, cc, id| (o)(&m.array, cc, id).map_store(Value::Array),
164        idx,
165    )
166}
167
168/// Applies a function to the register at the given index.
169pub fn apply_to_register<O, C>(
170    o: O,
171    idx: usize,
172) -> impl FnOnce(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
173where
174    O: FnOnce(&MvReg, &CausalContext, Identifier) -> CausalDotStore<MvReg>,
175    C: ExtensionType + fmt::Debug + PartialEq,
176{
177    apply(
178        move |m, cc, id| (o)(&m.reg, cc, id).map_store(Value::Register),
179        idx,
180    )
181}
182
183/// Moves an element from one index to another.
184pub fn mv<C>(
185    from: usize,
186    to: usize,
187) -> impl Fn(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
188where
189    C: ExtensionType + fmt::Debug + PartialEq,
190{
191    move |m, cc, id| {
192        let uid = uid_from_index(m, from);
193        let p = create_position_for_index(m, to);
194        m.mv(uid, p, cc, id)
195    }
196}
197
198/// Deletes an element at the given index.
199pub fn delete<'s, C>(
200    idx: usize,
201) -> impl Fn(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>> + 's
202where
203    C: ExtensionType + fmt::Debug + PartialEq,
204{
205    move |m, cc, id| {
206        let uid = uid_from_index(m, idx);
207        m.delete(uid, cc, id)
208    }
209}
210
211/// Clears the array.
212pub fn clear<C>() -> impl Fn(&OrArray<C>, &CausalContext, Identifier) -> CausalDotStore<OrArray<C>>
213where
214    C: ExtensionType + fmt::Debug + PartialEq,
215{
216    move |m, cc, id| m.clear(cc, id)
217}
218
219fn ids<C>(m: &OrArray<C>) -> Vec<((), Uid, Position)> {
220    // TODO(https://github.com/rust-lang/rust/issues/61695): use into_ok
221    m.with_list(|_, _, _| Ok::<_, Infallible>(Some(())))
222        .unwrap()
223}
224
225/// Computes the [`Position`] a new element should have to end up at `[idx]`.
226///
227/// Inserting a new element with the given [`Position`] will end up shifting all later elements to
228/// the rigth by one. For example, inserting an element with position `create_position_for_index(_,
229/// 0)` will make the current `[0]` be at `[1]`, the current `[1]` at `[2]`, and so on.
230fn create_position_for_index<C>(m: &OrArray<C>, idx: usize) -> Position {
231    // NOTE: the original code passes cc.id() to the Position::between calls here, but that
232    // argument is ignored, so it's removed in our implementation;
233
234    // we don't have to sort all the items to resolve the first/last position.
235    // not doing the sort saves us from the `.collect` in `with_list`, which would result in a
236    // `Vec` that gets pretty much immediately thrown away afterwards.
237    // TODO: cache min/max Position inside OrArray maybe?
238    if idx == 0 {
239        let min_p = m.iter_as_is().map(|(_, _, p)| p).min();
240        return Position::between(None, min_p);
241    }
242    if idx == m.len() {
243        let max_p = m.iter_as_is().map(|(_, _, p)| p).max();
244        return Position::between(max_p, None);
245    }
246
247    assert!(
248        idx < m.len(),
249        "index out of bounds ({idx} when length is {})",
250        m.len()
251    );
252    // NOTE: we know here that !m.is_empty(), otherwise we'd either hit idx == 0 or the asset.
253
254    let ids = ids(m);
255    let pos_at_index = ids.get(idx).map(|(_, _, p)| *p);
256    let pos_at_previous_index = if idx == 0 {
257        None
258    } else {
259        Some(
260            ids.get(idx - 1)
261                .expect("we check for out-of-bounds above")
262                .2,
263        )
264    };
265    Position::between(pos_at_previous_index, pos_at_index)
266}
267
268fn uid_from_index<C>(m: &OrArray<C>, idx: usize) -> Uid {
269    ids(m)[idx].1
270}