event-reduce 7.4.2__tar.gz

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.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: event-reduce
3
+ Version: 7.4.2
4
+ Summary: Update live query results from database change events without re-running the query.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/pubkey/event-reduce
7
+ Project-URL: Repository, https://github.com/pubkey/event-reduce
8
+ Keywords: live-query,reactive,realtime,database,bdd
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # event-reduce (Python)
13
+
14
+ Python implementation of the [EventReduce algorithm](https://github.com/pubkey/event-reduce).
15
+ It takes the previous results of a query and a change event of the database
16
+ and calculates the new results **in memory, without running the query over the database again**.
17
+
18
+ It uses the BDD that the [JavaScript implementation](../javascript/) created,
19
+ so both always decide on the same action. It needs Python 3.10 or newer and has no dependencies.
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ pip install event-reduce
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ from event_reduce import (
31
+ ActionName,
32
+ ChangeEvent,
33
+ StateResolveFunctionInput,
34
+ calculate_action_name,
35
+ dict_query_params,
36
+ run_action,
37
+ )
38
+
39
+
40
+ def compare(a, b):
41
+ # must never return 0 for two different documents, so sort by the primary key last
42
+ if a["age"] != b["age"]:
43
+ return -1 if a["age"] < b["age"] else 1
44
+ return -1 if a["id"] < b["id"] else 1
45
+
46
+
47
+ # SELECT * FROM users WHERE age > 18 ORDER BY age, id LIMIT 10
48
+ query_params = dict_query_params(
49
+ primary_key="id",
50
+ sort_fields=["age", "id"],
51
+ query_matcher=lambda doc: doc["age"] > 18,
52
+ sort_comparator=compare,
53
+ limit=10,
54
+ )
55
+
56
+ results = [{"id": "a", "age": 20}, {"id": "b", "age": 30}]
57
+
58
+ # a change event from the change stream of your database
59
+ change_event = ChangeEvent.insert("c", {"id": "c", "age": 25})
60
+
61
+ action = calculate_action_name(StateResolveFunctionInput(query_params, change_event, results))
62
+ if action == ActionName.RUN_FULL_QUERY_AGAIN:
63
+ results = run_query_on_database() # EventReduce could not optimize this event
64
+ else:
65
+ run_action(action, query_params, change_event, results)
66
+ # -> [a, c, b]
67
+ ```
68
+
69
+ `dict_query_params()` is for dict documents, like the `primaryKey` and `sortFields` of the JavaScript implementation.
70
+ The sort fields can be dot-separated paths like `"address.city"`.
71
+ For other documents, create the `QueryParams` with a `get_document_id` function
72
+ and a `sort_fields_changed(previous, doc)` function that returns true if a sort field changed.
73
+
74
+ ### Key document map
75
+
76
+ For big result sets, pass a dict from the id to the document as `key_document_map`
77
+ to both `StateResolveFunctionInput` and `run_action()`.
78
+ It is used to check whether a document is in the results without scanning them,
79
+ and `run_action()` keeps it in sync with the results.
80
+
81
+ ### Differences to the JavaScript implementation
82
+
83
+ - Change events are created with `ChangeEvent.insert()`, `ChangeEvent.update()` and `ChangeEvent.delete()`.
84
+ - The names are snake_case, `ActionName` and `StateName` are enums whose values are the JavaScript names.
85
+ - `run_action()` raises an `EventReduceError` for `RUN_FULL_QUERY_AGAIN` and `UNKNOWN_ACTION`.
86
+ `ALWAYS_WRONG` is only used by the truth-table generator and raises like `UNKNOWN_ACTION`.
87
+
88
+ ## Result window buffer
89
+
90
+ For a query with a limit or a skip, EventReduce returns `RUN_FULL_QUERY_AGAIN` when a row has to move into the result window that is not in the previous results:
91
+ the row after the window (when a row leaves a full window) or the last skipped row (when a row is added in front of the window).
92
+ The result window buffer avoids most of these full queries: it keeps `limit_buffer_size` rows after the window and `skip_buffer_size` skipped rows before it in memory.
93
+ It works like the [result window buffer of the JavaScript implementation](../docs/result-window-buffer.md),
94
+ which explains how it works, how to choose the buffer sizes and has measurements.
95
+
96
+ ```python
97
+ from event_reduce import ResultWindowBuffer, get_recommended_result_window_buffer_size
98
+
99
+ # recommended size: about log2(limit), at most 30
100
+ size = get_recommended_result_window_buffer_size(query_params.limit)
101
+ buffer = ResultWindowBuffer(query_params, limit_buffer_size=size, skip_buffer_size=size)
102
+ # run the query with the skip and limit of the buffer
103
+ buffer.reset(run_query(buffer.query()))
104
+ # the rows of the original skip and limit
105
+ results = buffer.results()
106
+
107
+ # on new change events
108
+ result = buffer.apply_change_events(change_events)
109
+ if result.run_full_query_again:
110
+ buffer.reset(run_query(buffer.query()))
111
+ results = buffer.results()
112
+ elif result.changed:
113
+ results = result.results
114
+ ```
115
+
116
+ `query()` returns the `skip` and `limit` to run the query with, `limit` is None if all rows after the skip are needed.
117
+ `math.inf` as a buffer size keeps all rows on that side, like `Infinity` in JavaScript.
118
+
119
+ ## Development
120
+
121
+ ```sh
122
+ pip install pytest ruff mypy
123
+ pytest
124
+ ruff check . && ruff format --check . && mypy src
125
+ ```
126
+
127
+ The BDD is generated by the truth-table generator of the JavaScript implementation,
128
+ which also writes `src/event_reduce/bdd_generated.py`.
129
+ The tests run the [shared test suite](../test-suite/), the same tests as all other implementations.
130
+
131
+ ## Who uses EventReduce
132
+
133
+ [RxDB](https://rxdb.info/), the local-first database, uses EventReduce for its live queries since [version 9.0.0](https://rxdb.info/releases/9.0.0.html), where it is enabled by default.
134
+ When an app subscribes to a query, RxDB keeps the results up to date by running each write event through EventReduce,
135
+ and only queries the storage again when EventReduce returns `RUN_FULL_QUERY_AGAIN`.
@@ -0,0 +1,124 @@
1
+ # event-reduce (Python)
2
+
3
+ Python implementation of the [EventReduce algorithm](https://github.com/pubkey/event-reduce).
4
+ It takes the previous results of a query and a change event of the database
5
+ and calculates the new results **in memory, without running the query over the database again**.
6
+
7
+ It uses the BDD that the [JavaScript implementation](../javascript/) created,
8
+ so both always decide on the same action. It needs Python 3.10 or newer and has no dependencies.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ pip install event-reduce
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```python
19
+ from event_reduce import (
20
+ ActionName,
21
+ ChangeEvent,
22
+ StateResolveFunctionInput,
23
+ calculate_action_name,
24
+ dict_query_params,
25
+ run_action,
26
+ )
27
+
28
+
29
+ def compare(a, b):
30
+ # must never return 0 for two different documents, so sort by the primary key last
31
+ if a["age"] != b["age"]:
32
+ return -1 if a["age"] < b["age"] else 1
33
+ return -1 if a["id"] < b["id"] else 1
34
+
35
+
36
+ # SELECT * FROM users WHERE age > 18 ORDER BY age, id LIMIT 10
37
+ query_params = dict_query_params(
38
+ primary_key="id",
39
+ sort_fields=["age", "id"],
40
+ query_matcher=lambda doc: doc["age"] > 18,
41
+ sort_comparator=compare,
42
+ limit=10,
43
+ )
44
+
45
+ results = [{"id": "a", "age": 20}, {"id": "b", "age": 30}]
46
+
47
+ # a change event from the change stream of your database
48
+ change_event = ChangeEvent.insert("c", {"id": "c", "age": 25})
49
+
50
+ action = calculate_action_name(StateResolveFunctionInput(query_params, change_event, results))
51
+ if action == ActionName.RUN_FULL_QUERY_AGAIN:
52
+ results = run_query_on_database() # EventReduce could not optimize this event
53
+ else:
54
+ run_action(action, query_params, change_event, results)
55
+ # -> [a, c, b]
56
+ ```
57
+
58
+ `dict_query_params()` is for dict documents, like the `primaryKey` and `sortFields` of the JavaScript implementation.
59
+ The sort fields can be dot-separated paths like `"address.city"`.
60
+ For other documents, create the `QueryParams` with a `get_document_id` function
61
+ and a `sort_fields_changed(previous, doc)` function that returns true if a sort field changed.
62
+
63
+ ### Key document map
64
+
65
+ For big result sets, pass a dict from the id to the document as `key_document_map`
66
+ to both `StateResolveFunctionInput` and `run_action()`.
67
+ It is used to check whether a document is in the results without scanning them,
68
+ and `run_action()` keeps it in sync with the results.
69
+
70
+ ### Differences to the JavaScript implementation
71
+
72
+ - Change events are created with `ChangeEvent.insert()`, `ChangeEvent.update()` and `ChangeEvent.delete()`.
73
+ - The names are snake_case, `ActionName` and `StateName` are enums whose values are the JavaScript names.
74
+ - `run_action()` raises an `EventReduceError` for `RUN_FULL_QUERY_AGAIN` and `UNKNOWN_ACTION`.
75
+ `ALWAYS_WRONG` is only used by the truth-table generator and raises like `UNKNOWN_ACTION`.
76
+
77
+ ## Result window buffer
78
+
79
+ For a query with a limit or a skip, EventReduce returns `RUN_FULL_QUERY_AGAIN` when a row has to move into the result window that is not in the previous results:
80
+ the row after the window (when a row leaves a full window) or the last skipped row (when a row is added in front of the window).
81
+ The result window buffer avoids most of these full queries: it keeps `limit_buffer_size` rows after the window and `skip_buffer_size` skipped rows before it in memory.
82
+ It works like the [result window buffer of the JavaScript implementation](../docs/result-window-buffer.md),
83
+ which explains how it works, how to choose the buffer sizes and has measurements.
84
+
85
+ ```python
86
+ from event_reduce import ResultWindowBuffer, get_recommended_result_window_buffer_size
87
+
88
+ # recommended size: about log2(limit), at most 30
89
+ size = get_recommended_result_window_buffer_size(query_params.limit)
90
+ buffer = ResultWindowBuffer(query_params, limit_buffer_size=size, skip_buffer_size=size)
91
+ # run the query with the skip and limit of the buffer
92
+ buffer.reset(run_query(buffer.query()))
93
+ # the rows of the original skip and limit
94
+ results = buffer.results()
95
+
96
+ # on new change events
97
+ result = buffer.apply_change_events(change_events)
98
+ if result.run_full_query_again:
99
+ buffer.reset(run_query(buffer.query()))
100
+ results = buffer.results()
101
+ elif result.changed:
102
+ results = result.results
103
+ ```
104
+
105
+ `query()` returns the `skip` and `limit` to run the query with, `limit` is None if all rows after the skip are needed.
106
+ `math.inf` as a buffer size keeps all rows on that side, like `Infinity` in JavaScript.
107
+
108
+ ## Development
109
+
110
+ ```sh
111
+ pip install pytest ruff mypy
112
+ pytest
113
+ ruff check . && ruff format --check . && mypy src
114
+ ```
115
+
116
+ The BDD is generated by the truth-table generator of the JavaScript implementation,
117
+ which also writes `src/event_reduce/bdd_generated.py`.
118
+ The tests run the [shared test suite](../test-suite/), the same tests as all other implementations.
119
+
120
+ ## Who uses EventReduce
121
+
122
+ [RxDB](https://rxdb.info/), the local-first database, uses EventReduce for its live queries since [version 9.0.0](https://rxdb.info/releases/9.0.0.html), where it is enabled by default.
123
+ When an app subscribes to a query, RxDB keeps the results up to date by running each write event through EventReduce,
124
+ and only queries the storage again when EventReduce returns `RUN_FULL_QUERY_AGAIN`.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "event-reduce"
7
+ version = "7.4.2"
8
+ description = "Update live query results from database change events without re-running the query."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ keywords = ["live-query", "reactive", "realtime", "database", "bdd"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/pubkey/event-reduce"
16
+ Repository = "https://github.com/pubkey/event-reduce"
17
+
18
+ [tool.setuptools.package-data]
19
+ event_reduce = ["py.typed"]
20
+
21
+ [tool.pytest.ini_options]
22
+ testpaths = ["tests"]
23
+ pythonpath = ["src"]
24
+
25
+ [tool.ruff]
26
+ line-length = 120
27
+ target-version = "py310"
28
+
29
+ [tool.ruff.lint]
30
+ select = ["E", "F", "W", "I", "UP", "B"]
31
+
32
+ [tool.ruff.lint.per-file-ignores]
33
+ # generated, the packed BDD string is one long line
34
+ "src/event_reduce/bdd_generated.py" = ["E501"]
35
+
36
+ [tool.mypy]
37
+ strict = true
38
+ python_version = "3.10"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,89 @@
1
+ """
2
+ EventReduce: update live query results from change events without re-running the query.
3
+
4
+ Only the BDD that the JavaScript implementation generated is used, it is not created or optimized here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, TypeVar
10
+
11
+ from .actions import ACTION_FUNCTION_LIST, ORDERED_ACTION_LIST, ActionFunction
12
+ from .bdd import resolve_input
13
+ from .bdd_generated import PACKED_BDD
14
+ from .result_window_buffer import (
15
+ ResultWindowBuffer,
16
+ ResultWindowBufferChangeResult,
17
+ ResultWindowBufferQuery,
18
+ ResultWindowBufferResult,
19
+ get_recommended_result_window_buffer_size,
20
+ )
21
+ from .states import ORDERED_STATE_LIST, get_state_set, resolve_state
22
+ from .types import (
23
+ ActionName,
24
+ ChangeEvent,
25
+ EventReduceError,
26
+ QueryParams,
27
+ ResultKeyDocumentMap,
28
+ StateName,
29
+ StateResolveFunctionInput,
30
+ WriteOperation,
31
+ )
32
+ from .util import dict_query_params, get_property
33
+
34
+ D = TypeVar("D")
35
+
36
+
37
+ def calculate_action_name(input: StateResolveFunctionInput[Any]) -> ActionName:
38
+ """
39
+ Returns the action that transforms the previous results into the new results.
40
+ If it is RUN_FULL_QUERY_AGAIN, the query must be run over the database again.
41
+ """
42
+ return ORDERED_ACTION_LIST[resolve_input(input)]
43
+
44
+
45
+ def run_action(
46
+ action: ActionName,
47
+ query_params: QueryParams[D],
48
+ change_event: ChangeEvent[D],
49
+ previous_results: list[D],
50
+ key_document_map: dict[str, D] | None = None,
51
+ ) -> list[D]:
52
+ """
53
+ Applies the action to the results.
54
+ For performance reasons, it mutates previous_results and the key_document_map
55
+ and returns the mutated results.
56
+ Raises an EventReduceError for RUN_FULL_QUERY_AGAIN and UNKNOWN_ACTION.
57
+ """
58
+ ACTION_FUNCTION_LIST[ORDERED_ACTION_LIST.index(action)](
59
+ StateResolveFunctionInput(query_params, change_event, previous_results, key_document_map)
60
+ )
61
+ return previous_results
62
+
63
+
64
+ __all__ = [
65
+ "ORDERED_ACTION_LIST",
66
+ "ORDERED_STATE_LIST",
67
+ "PACKED_BDD",
68
+ "ActionFunction",
69
+ "ActionName",
70
+ "ChangeEvent",
71
+ "EventReduceError",
72
+ "QueryParams",
73
+ "ResultKeyDocumentMap",
74
+ "ResultWindowBuffer",
75
+ "ResultWindowBufferChangeResult",
76
+ "ResultWindowBufferQuery",
77
+ "ResultWindowBufferResult",
78
+ "StateName",
79
+ "StateResolveFunctionInput",
80
+ "WriteOperation",
81
+ "calculate_action_name",
82
+ "dict_query_params",
83
+ "get_property",
84
+ "get_recommended_result_window_buffer_size",
85
+ "get_state_set",
86
+ "resolve_input",
87
+ "resolve_state",
88
+ "run_action",
89
+ ]
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from .types import ActionName, EventReduceError, StateResolveFunctionInput
7
+ from .util import index_of_document_id, push_at_sort_position
8
+
9
+ ActionFunction = Callable[[StateResolveFunctionInput[Any]], None]
10
+ """For performance reasons, action functions mutate the results and the key document map of the input."""
11
+
12
+ ORDERED_ACTION_LIST: list[ActionName] = list(ActionName)
13
+ """All actions, in the same order as the JavaScript implementation. The BDD references the actions by their index."""
14
+
15
+
16
+ def _find_index_of_changed_doc(input: StateResolveFunctionInput[Any]) -> int:
17
+ """Returns the index of the changed document inside of the previous results or -1."""
18
+ id = input.change_event.id
19
+ if input.key_document_map is not None and id not in input.key_document_map:
20
+ return -1
21
+ return index_of_document_id(input.query_params, input.previous_results, id)
22
+
23
+
24
+ def _changed_doc(input: StateResolveFunctionInput[Any]) -> Any:
25
+ """The new document of the change event. Only the BDD knows that it exists for the given action."""
26
+ doc = input.change_event.doc
27
+ if doc is None:
28
+ raise EventReduceError("UNK_ACT")
29
+ return doc
30
+
31
+
32
+ def _add_to_map(input: StateResolveFunctionInput[Any], doc: Any) -> None:
33
+ if input.key_document_map is not None:
34
+ input.key_document_map[input.change_event.id] = doc
35
+
36
+
37
+ def _remove_from_map(input: StateResolveFunctionInput[Any], id: str) -> None:
38
+ if input.key_document_map is not None:
39
+ input.key_document_map.pop(id, None)
40
+
41
+
42
+ def do_nothing(input: StateResolveFunctionInput[Any]) -> None:
43
+ pass
44
+
45
+
46
+ def insert_first(input: StateResolveFunctionInput[Any]) -> None:
47
+ doc = _changed_doc(input)
48
+ input.previous_results.insert(0, doc)
49
+ _add_to_map(input, doc)
50
+
51
+
52
+ def insert_last(input: StateResolveFunctionInput[Any]) -> None:
53
+ doc = _changed_doc(input)
54
+ input.previous_results.append(doc)
55
+ _add_to_map(input, doc)
56
+
57
+
58
+ def remove_first_item(input: StateResolveFunctionInput[Any]) -> None:
59
+ if not input.previous_results:
60
+ return
61
+ first = input.previous_results.pop(0)
62
+ _remove_from_map(input, input.query_params.get_document_id(first))
63
+
64
+
65
+ def remove_last_item(input: StateResolveFunctionInput[Any]) -> None:
66
+ if not input.previous_results:
67
+ return
68
+ last = input.previous_results.pop()
69
+ _remove_from_map(input, input.query_params.get_document_id(last))
70
+
71
+
72
+ def remove_first_insert_last(input: StateResolveFunctionInput[Any]) -> None:
73
+ remove_first_item(input)
74
+ insert_last(input)
75
+
76
+
77
+ def remove_last_insert_first(input: StateResolveFunctionInput[Any]) -> None:
78
+ remove_last_item(input)
79
+ insert_first(input)
80
+
81
+
82
+ def remove_first_insert_first(input: StateResolveFunctionInput[Any]) -> None:
83
+ remove_first_item(input)
84
+ insert_first(input)
85
+
86
+
87
+ def remove_last_insert_last(input: StateResolveFunctionInput[Any]) -> None:
88
+ remove_last_item(input)
89
+ insert_last(input)
90
+
91
+
92
+ def remove_existing(input: StateResolveFunctionInput[Any]) -> None:
93
+ index = _find_index_of_changed_doc(input)
94
+ _remove_from_map(input, input.change_event.id)
95
+ if index >= 0:
96
+ del input.previous_results[index]
97
+
98
+
99
+ def replace_existing(input: StateResolveFunctionInput[Any]) -> None:
100
+ index = _find_index_of_changed_doc(input)
101
+ if index >= 0:
102
+ doc = _changed_doc(input)
103
+ input.previous_results[index] = doc
104
+ _add_to_map(input, doc)
105
+
106
+
107
+ def insert_at_sort_position(input: StateResolveFunctionInput[Any]) -> None:
108
+ doc = _changed_doc(input)
109
+ id = input.change_event.id
110
+ key_document_map = input.key_document_map
111
+ # If document is already in results,
112
+ # we cannot add it again because it would throw on non-deterministic ordering.
113
+ if key_document_map is not None:
114
+ if id in key_document_map:
115
+ return
116
+ key_document_map[id] = doc
117
+ elif index_of_document_id(input.query_params, input.previous_results, id) >= 0:
118
+ return
119
+ push_at_sort_position(input.previous_results, doc, input.query_params.sort_comparator)
120
+
121
+
122
+ def remove_existing_and_insert_at_sort_position(input: StateResolveFunctionInput[Any]) -> None:
123
+ remove_existing(input)
124
+ insert_at_sort_position(input)
125
+
126
+
127
+ def _insert_at_sort_position_and_remove(input: StateResolveFunctionInput[Any], remove: ActionFunction) -> None:
128
+ length_before = len(input.previous_results)
129
+ insert_at_sort_position(input)
130
+ # insert_at_sort_position() does nothing if the document is already in the results
131
+ if len(input.previous_results) > length_before:
132
+ remove(input)
133
+
134
+
135
+ def insert_at_sort_position_remove_last(input: StateResolveFunctionInput[Any]) -> None:
136
+ """
137
+ Inserts the document at its sort position and then removes the last document.
138
+ Used when the limit was reached and the new document
139
+ sorts in between the first and the last document,
140
+ so the previous last document is pushed out of the results.
141
+ """
142
+ _insert_at_sort_position_and_remove(input, remove_last_item)
143
+
144
+
145
+ def insert_at_sort_position_remove_first(input: StateResolveFunctionInput[Any]) -> None:
146
+ """
147
+ Inserts the document at its sort position and then removes the first document.
148
+ Used when a document moves out of the skipped documents into the results,
149
+ so the previous first document is pushed into the skipped documents.
150
+ """
151
+ _insert_at_sort_position_and_remove(input, remove_first_item)
152
+
153
+
154
+ def run_full_query_again(input: StateResolveFunctionInput[Any]) -> None:
155
+ raise EventReduceError("RUN_FULL")
156
+
157
+
158
+ def unknown_action(input: StateResolveFunctionInput[Any]) -> None:
159
+ raise EventReduceError("UNK_ACT")
160
+
161
+
162
+ ACTION_FUNCTION_LIST: list[ActionFunction] = [
163
+ do_nothing,
164
+ insert_first,
165
+ insert_last,
166
+ remove_first_item,
167
+ remove_last_item,
168
+ remove_first_insert_last,
169
+ remove_last_insert_first,
170
+ remove_first_insert_first,
171
+ remove_last_insert_last,
172
+ remove_existing,
173
+ replace_existing,
174
+ # alwaysWrong is only used by the truth-table generator and never returned by the BDD
175
+ unknown_action,
176
+ insert_at_sort_position,
177
+ remove_existing_and_insert_at_sort_position,
178
+ insert_at_sort_position_remove_last,
179
+ insert_at_sort_position_remove_first,
180
+ run_full_query_again,
181
+ unknown_action,
182
+ ]
183
+ """The action functions in the same order as ORDERED_ACTION_LIST."""
@@ -0,0 +1,38 @@
1
+ """
2
+ Resolves the action with the BDD of the JavaScript implementation.
3
+ Only the already generated BDD is used, it is not created or optimized here.
4
+
5
+ The BDD is a PackedBdd of the binary-decision-diagram npm package,
6
+ bdd_generated.PACKED_BDD holds its decoded entries.
7
+
8
+ Layout of the entries:
9
+ - Every non-leaf node takes three consecutive entries: [level, ref0, ref1]
10
+ - level is the index of the state function in ORDERED_STATE_LIST
11
+ - ref0 is followed when the state is false, ref1 when it is true
12
+ - The node with the id x starts at index 3 * x, the root node has the id 0.
13
+ - With n = len(entries) / 3, a reference r < n points to the node with the id r,
14
+ r >= n is a leaf with the action index r - n in ORDERED_ACTION_LIST.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ from .bdd_generated import PACKED_BDD
22
+ from .states import STATE_RESOLVE_FUNCTION_BY_INDEX
23
+ from .types import StateResolveFunctionInput
24
+
25
+ _NODE_COUNT = len(PACKED_BDD) // 3
26
+
27
+
28
+ def resolve_input(input: StateResolveFunctionInput[Any]) -> int:
29
+ """Walks the BDD and returns the index of the action in ORDERED_ACTION_LIST."""
30
+ bdd = PACKED_BDD
31
+ fns = STATE_RESOLVE_FUNCTION_BY_INDEX
32
+ n = _NODE_COUNT
33
+ i = 0
34
+ while True:
35
+ ref = bdd[i + 2] if fns[bdd[i]](input) else bdd[i + 1]
36
+ if ref >= n:
37
+ return ref - n
38
+ i = ref * 3
@@ -0,0 +1,4 @@
1
+ # Code generated by the truth-table-generator of the JavaScript implementation. DO NOT EDIT.
2
+ # Run `npm run optimize-bdd` in ./javascript instead.
3
+
4
+ PACKED_BDD: tuple[int, ...] = (1, 1, 2, 0, 3, 4, 17, 5, 6, 6, 7, 92, 17, 92, 8, 6, 9, 92, 6, 10, 11, 11, 12, 13, 6, 14, 15, 11, 16, 13, 11, 17, 18, 5, 93, 19, 5, 92, 20, 7, 21, 108, 5, 22, 23, 5, 93, 108, 5, 92, 24, 5, 22, 25, 5, 26, 27, 16, 108, 92, 12, 92, 28, 8, 29, 95, 7, 30, 31, 7, 32, 33, 10, 34, 20, 10, 35, 36, 10, 37, 38, 10, 37, 39, 7, 40, 41, 9, 101, 96, 15, 42, 94, 15, 43, 92, 15, 44, 94, 15, 45, 92, 7, 46, 47, 7, 48, 49, 12, 23, 50, 8, 51, 52, 7, 53, 54, 7, 55, 56, 16, 92, 95, 16, 92, 108, 14, 104, 93, 4, 57, 97, 14, 104, 108, 4, 58, 108, 15, 59, 92, 15, 60, 92, 15, 61, 94, 15, 62, 92, 7, 63, 64, 9, 102, 100, 9, 99, 97, 8, 65, 66, 8, 67, 68, 8, 69, 70, 8, 71, 72, 14, 106, 98, 14, 106, 108, 14, 92, 40, 4, 73, 41, 14, 104, 19, 4, 74, 19, 15, 75, 76, 15, 77, 41, 9, 105, 78, 9, 79, 97, 9, 80, 81, 9, 82, 108, 9, 83, 84, 9, 85, 108, 15, 86, 108, 9, 71, 108, 14, 92, 41, 14, 106, 19, 14, 87, 19, 16, 94, 97, 4, 88, 19, 15, 89, 100, 15, 90, 97, 15, 105, 108, 15, 89, 108, 15, 90, 108, 15, 86, 105, 15, 86, 100, 15, 86, 97, 14, 105, 108, 16, 104, 107, 14, 91, 19, 14, 105, 98, 14, 105, 99, 16, 106, 107) # fmt: skip
File without changes