tigrbl-kernel 0.4.2.dev3__py3-none-any.whl → 0.4.3.dev4__py3-none-any.whl
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.
- tigrbl_kernel/__init__.py +1 -19
- tigrbl_kernel/_build.py +1 -1
- tigrbl_kernel/_compile.py +100 -1
- tigrbl_kernel/channel_taxonomy.py +142 -0
- tigrbl_kernel/cross_transport.py +167 -0
- tigrbl_kernel/dispatch_taxonomy.py +47 -0
- tigrbl_kernel/events.py +10 -0
- tigrbl_kernel/helpers.py +2 -2
- tigrbl_kernel/loop_modes.py +19 -1
- tigrbl_kernel/measure.py +1 -1
- tigrbl_kernel/ordering.py +1 -0
- tigrbl_kernel/packed_access.py +131 -0
- tigrbl_kernel/packed_selectors.py +239 -0
- tigrbl_kernel/protocol_anchors.py +49 -0
- tigrbl_kernel/protocol_bindings.py +26 -7
- tigrbl_kernel/protocol_chains/http_stream.py +35 -2
- tigrbl_kernel/protocol_streams.py +470 -0
- tigrbl_kernel/resume_policy.py +78 -0
- tigrbl_kernel/runtime_contracts.py +254 -0
- tigrbl_kernel/rust_compile.py +11 -27
- tigrbl_kernel/rust_plan.py +9 -3
- tigrbl_kernel/rust_spec.py +15 -398
- tigrbl_kernel/transport_events.py +5 -2
- tigrbl_kernel/types.py +1 -0
- tigrbl_kernel/webtransport_events.py +65 -3
- {tigrbl_kernel-0.4.2.dev3.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/METADATA +183 -28
- {tigrbl_kernel-0.4.2.dev3.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/RECORD +30 -20
- tigrbl_kernel-0.4.3.dev4.dist-info/licenses/NOTICE +7 -0
- {tigrbl_kernel-0.4.2.dev3.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/WHEEL +0 -0
- {tigrbl_kernel-0.4.2.dev3.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
import zlib
|
|
6
|
+
|
|
7
|
+
from .models import PackedHotSection, PackedHotSectionDirectory, PackedKernel
|
|
8
|
+
|
|
9
|
+
HOT_RUNNER_GENERIC = 0
|
|
10
|
+
HOT_RUNNER_LINEAR_DIRECT = 1
|
|
11
|
+
HOT_RUNNER_COMPILED_PARAM = 2
|
|
12
|
+
HOT_RUNNER_WS_UNARY_TEXT = 3
|
|
13
|
+
|
|
14
|
+
DIRECT_INVOKE_STEP = 0
|
|
15
|
+
DIRECT_INVOKE_RUN = 1
|
|
16
|
+
DIRECT_INVOKE_RUN_WITH_NONE = 2
|
|
17
|
+
DIRECT_INVOKE_RUN_WITH_DEP = 3
|
|
18
|
+
|
|
19
|
+
TRANSPORT_KIND_GENERIC = 0
|
|
20
|
+
TRANSPORT_KIND_REST = 1
|
|
21
|
+
TRANSPORT_KIND_JSONRPC = 2
|
|
22
|
+
TRANSPORT_KIND_CHANNEL = 3
|
|
23
|
+
|
|
24
|
+
HTTP_METHOD_ID_BY_NAME = {
|
|
25
|
+
"GET": 1,
|
|
26
|
+
"HEAD": 2,
|
|
27
|
+
"POST": 3,
|
|
28
|
+
"PUT": 4,
|
|
29
|
+
"PATCH": 5,
|
|
30
|
+
"DELETE": 6,
|
|
31
|
+
"OPTIONS": 7,
|
|
32
|
+
"TRACE": 8,
|
|
33
|
+
"CONNECT": 9,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def hot_block_view(packed: PackedKernel) -> Mapping[str, Any]:
|
|
38
|
+
view = getattr(packed, "hot_block_view", None)
|
|
39
|
+
return view if isinstance(view, Mapping) else {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def hot_block_sections(packed: PackedKernel) -> PackedHotSectionDirectory | None:
|
|
43
|
+
sections = getattr(packed, "hot_block_sections", None)
|
|
44
|
+
return sections if isinstance(sections, PackedHotSectionDirectory) else None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def hot_section(packed: PackedKernel, key: str) -> PackedHotSection | None:
|
|
48
|
+
directory = hot_block_sections(packed)
|
|
49
|
+
if directory is None:
|
|
50
|
+
return None
|
|
51
|
+
return directory.get(key)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def hot_array(
|
|
55
|
+
packed: PackedKernel,
|
|
56
|
+
key: str,
|
|
57
|
+
fallback: tuple[Any, ...] | tuple[int, ...] | tuple[str, ...],
|
|
58
|
+
) -> tuple[Any, ...]:
|
|
59
|
+
values = hot_block_view(packed).get(key)
|
|
60
|
+
if isinstance(values, tuple):
|
|
61
|
+
return values
|
|
62
|
+
if isinstance(values, list):
|
|
63
|
+
return tuple(values)
|
|
64
|
+
return fallback
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def hot_int_at(
|
|
68
|
+
packed: PackedKernel,
|
|
69
|
+
key: str,
|
|
70
|
+
index: int,
|
|
71
|
+
fallback: tuple[int, ...] | tuple[Any, ...],
|
|
72
|
+
) -> int | None:
|
|
73
|
+
section = hot_section(packed, key)
|
|
74
|
+
if section is not None:
|
|
75
|
+
if 0 <= index < int(section.count):
|
|
76
|
+
return section.get_int(index)
|
|
77
|
+
return None
|
|
78
|
+
if 0 <= index < len(fallback):
|
|
79
|
+
return int(fallback[index])
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def hot_count(
|
|
84
|
+
packed: PackedKernel,
|
|
85
|
+
key: str,
|
|
86
|
+
fallback: tuple[int, ...] | tuple[Any, ...] | tuple[str, ...],
|
|
87
|
+
) -> int:
|
|
88
|
+
section = hot_section(packed, key)
|
|
89
|
+
if section is not None:
|
|
90
|
+
return int(section.count)
|
|
91
|
+
return len(fallback)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def stable_name_hash64(value: str, *, lowercase: bool = False) -> int:
|
|
95
|
+
normalized = value.lower() if lowercase else value
|
|
96
|
+
encoded = normalized.encode("utf-8")
|
|
97
|
+
lo = zlib.crc32(encoded) & 0xFFFFFFFF
|
|
98
|
+
hi = zlib.crc32(encoded, 0x9E3779B9) & 0xFFFFFFFF
|
|
99
|
+
return (hi << 32) | lo
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def http_method_id(method: str) -> int:
|
|
103
|
+
normalized = str(method or "").upper()
|
|
104
|
+
cached = HTTP_METHOD_ID_BY_NAME.get(normalized)
|
|
105
|
+
if cached is not None:
|
|
106
|
+
return cached
|
|
107
|
+
return 1024 + (stable_name_hash64(normalized) & 0xFFFF)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def coerce_int(value: Any) -> int | None:
|
|
111
|
+
return value if isinstance(value, int) else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def coerce_dict(value: Any) -> Mapping[str, Any]:
|
|
115
|
+
return value if isinstance(value, Mapping) else {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_program_hot_runner_id(
|
|
119
|
+
packed: PackedKernel,
|
|
120
|
+
program_id: int,
|
|
121
|
+
hot_op_plan: Any | None,
|
|
122
|
+
) -> int:
|
|
123
|
+
if hot_op_plan is not None:
|
|
124
|
+
program_hot_runner_id = coerce_int(
|
|
125
|
+
getattr(hot_op_plan, "program_hot_runner_id", None)
|
|
126
|
+
)
|
|
127
|
+
if program_hot_runner_id is not None:
|
|
128
|
+
return program_hot_runner_id
|
|
129
|
+
fallback = tuple(getattr(packed, "program_hot_runner_ids", ()) or ())
|
|
130
|
+
value = hot_int_at(packed, "program_hot_runner_ids", program_id, fallback)
|
|
131
|
+
return int(value) if value is not None else HOT_RUNNER_GENERIC
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, MutableMapping
|
|
4
|
+
|
|
5
|
+
from .models import KernelPlan, PackedKernel
|
|
6
|
+
from .packed_access import (
|
|
7
|
+
hot_array,
|
|
8
|
+
hot_section,
|
|
9
|
+
http_method_id,
|
|
10
|
+
stable_name_hash64,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
ExactRouteCache = MutableMapping[int, Mapping[int, tuple[int, int]]]
|
|
14
|
+
ExactRouteVerifyCache = MutableMapping[
|
|
15
|
+
int, Mapping[int, Mapping[int, tuple[tuple[str, int], ...]]]
|
|
16
|
+
]
|
|
17
|
+
ExactWebSocketCache = MutableMapping[int, Mapping[tuple[str, str], int]]
|
|
18
|
+
ExactJsonRpcCache = MutableMapping[int, Mapping[str, Mapping[str, tuple[int, str, str]]]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_hot_exact_route_slices(
|
|
22
|
+
packed: PackedKernel,
|
|
23
|
+
cache: ExactRouteCache,
|
|
24
|
+
) -> Mapping[int, tuple[int, int]]:
|
|
25
|
+
packed_id = id(packed)
|
|
26
|
+
cached = cache.get(packed_id)
|
|
27
|
+
if cached is not None:
|
|
28
|
+
return cached
|
|
29
|
+
method_ids = hot_section(packed, "exact_method_ids")
|
|
30
|
+
path_hashes = hot_section(packed, "exact_path_hashes")
|
|
31
|
+
program_ids = hot_section(packed, "exact_program_ids")
|
|
32
|
+
if method_ids is None or path_hashes is None or program_ids is None:
|
|
33
|
+
cache[packed_id] = {}
|
|
34
|
+
return {}
|
|
35
|
+
if not (
|
|
36
|
+
int(method_ids.count) == int(path_hashes.count) == int(program_ids.count)
|
|
37
|
+
):
|
|
38
|
+
cache[packed_id] = {}
|
|
39
|
+
return {}
|
|
40
|
+
directory: dict[int, tuple[int, int]] = {}
|
|
41
|
+
total = int(method_ids.count)
|
|
42
|
+
current_method_id = -1
|
|
43
|
+
current_start = 0
|
|
44
|
+
current_count = 0
|
|
45
|
+
for index in range(total):
|
|
46
|
+
method_id = int(method_ids.get_int(index))
|
|
47
|
+
if method_id == current_method_id:
|
|
48
|
+
current_count += 1
|
|
49
|
+
continue
|
|
50
|
+
if current_count > 0:
|
|
51
|
+
directory[current_method_id] = (current_start, current_count)
|
|
52
|
+
current_method_id = method_id
|
|
53
|
+
current_start = index
|
|
54
|
+
current_count = 1
|
|
55
|
+
if current_count > 0:
|
|
56
|
+
directory[current_method_id] = (current_start, current_count)
|
|
57
|
+
frozen = {
|
|
58
|
+
int(method_id): (int(start), int(count))
|
|
59
|
+
for method_id, (start, count) in directory.items()
|
|
60
|
+
}
|
|
61
|
+
cache[packed_id] = frozen
|
|
62
|
+
return frozen
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_hot_exact_route_verify(
|
|
66
|
+
packed: PackedKernel,
|
|
67
|
+
cache: ExactRouteVerifyCache,
|
|
68
|
+
) -> Mapping[int, Mapping[int, tuple[tuple[str, int], ...]]]:
|
|
69
|
+
packed_id = id(packed)
|
|
70
|
+
cached = cache.get(packed_id)
|
|
71
|
+
if cached is not None:
|
|
72
|
+
return cached
|
|
73
|
+
|
|
74
|
+
route = getattr(packed, "rest_exact_route_to_program", None)
|
|
75
|
+
if not isinstance(route, Mapping):
|
|
76
|
+
cache[packed_id] = {}
|
|
77
|
+
return {}
|
|
78
|
+
|
|
79
|
+
verify: dict[int, dict[int, list[tuple[str, int]]]] = {}
|
|
80
|
+
for route_key, program_id in route.items():
|
|
81
|
+
if (
|
|
82
|
+
not isinstance(route_key, tuple)
|
|
83
|
+
or len(route_key) != 2
|
|
84
|
+
or not isinstance(route_key[0], str)
|
|
85
|
+
or not isinstance(route_key[1], str)
|
|
86
|
+
or not isinstance(program_id, int)
|
|
87
|
+
):
|
|
88
|
+
continue
|
|
89
|
+
method_name, exact_path = route_key
|
|
90
|
+
method_id = http_method_id(method_name)
|
|
91
|
+
path_hash = stable_name_hash64(exact_path)
|
|
92
|
+
method_bucket = verify.setdefault(method_id, {})
|
|
93
|
+
method_bucket.setdefault(path_hash, []).append((exact_path, program_id))
|
|
94
|
+
|
|
95
|
+
frozen = {
|
|
96
|
+
int(method_id): {
|
|
97
|
+
int(path_hash): tuple(entries)
|
|
98
|
+
for path_hash, entries in method_bucket.items()
|
|
99
|
+
}
|
|
100
|
+
for method_id, method_bucket in verify.items()
|
|
101
|
+
}
|
|
102
|
+
cache[packed_id] = frozen
|
|
103
|
+
return frozen
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolve_program_id_from_exact_route(
|
|
107
|
+
packed: PackedKernel,
|
|
108
|
+
method: str,
|
|
109
|
+
path: str,
|
|
110
|
+
route_cache: ExactRouteCache,
|
|
111
|
+
verify_cache: ExactRouteVerifyCache,
|
|
112
|
+
) -> int:
|
|
113
|
+
method_id = http_method_id(method)
|
|
114
|
+
path_hash = stable_name_hash64(path)
|
|
115
|
+
path_hashes = hot_section(packed, "exact_path_hashes")
|
|
116
|
+
program_ids = hot_section(packed, "exact_program_ids")
|
|
117
|
+
method_slices = resolve_hot_exact_route_slices(packed, route_cache)
|
|
118
|
+
method_slice = method_slices.get(method_id)
|
|
119
|
+
if (
|
|
120
|
+
method_slice is not None
|
|
121
|
+
and path_hashes is not None
|
|
122
|
+
and program_ids is not None
|
|
123
|
+
and int(path_hashes.count) == int(program_ids.count)
|
|
124
|
+
):
|
|
125
|
+
start_index, count = method_slice
|
|
126
|
+
found_index = path_hashes.find_aligned_u64(
|
|
127
|
+
path_hash,
|
|
128
|
+
start_index=start_index,
|
|
129
|
+
count=count,
|
|
130
|
+
)
|
|
131
|
+
if start_index <= found_index < start_index + count:
|
|
132
|
+
program_id = int(program_ids.get_int(found_index))
|
|
133
|
+
verify = resolve_hot_exact_route_verify(packed, verify_cache)
|
|
134
|
+
method_verify = verify.get(method_id, {})
|
|
135
|
+
candidates = method_verify.get(path_hash, ())
|
|
136
|
+
if candidates:
|
|
137
|
+
for candidate_path, candidate_program_id in candidates:
|
|
138
|
+
if candidate_path == path:
|
|
139
|
+
return int(candidate_program_id)
|
|
140
|
+
return -1
|
|
141
|
+
return program_id
|
|
142
|
+
method_ids = hot_array(packed, "exact_method_ids", tuple())
|
|
143
|
+
path_hash_array = hot_array(packed, "exact_path_hashes", tuple())
|
|
144
|
+
program_id_array = hot_array(packed, "exact_program_ids", tuple())
|
|
145
|
+
for candidate_method_id, candidate_hash, program_id in zip(
|
|
146
|
+
method_ids, path_hash_array, program_id_array
|
|
147
|
+
):
|
|
148
|
+
if int(candidate_method_id) == int(method_id) and int(candidate_hash) == int(
|
|
149
|
+
path_hash
|
|
150
|
+
):
|
|
151
|
+
return int(program_id)
|
|
152
|
+
route = getattr(packed, "rest_exact_route_to_program", None)
|
|
153
|
+
if not isinstance(route, Mapping):
|
|
154
|
+
return -1
|
|
155
|
+
maybe = route.get((method.upper(), path))
|
|
156
|
+
return maybe if isinstance(maybe, int) else -1
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def resolve_hot_exact_websocket_routes(
|
|
160
|
+
plan: KernelPlan,
|
|
161
|
+
packed: PackedKernel,
|
|
162
|
+
cache: ExactWebSocketCache,
|
|
163
|
+
) -> Mapping[tuple[str, str], int]:
|
|
164
|
+
packed_id = id(packed)
|
|
165
|
+
cached = cache.get(packed_id)
|
|
166
|
+
if cached is not None:
|
|
167
|
+
return cached
|
|
168
|
+
exact: dict[tuple[str, str], int] = {}
|
|
169
|
+
for proto in ("ws", "wss"):
|
|
170
|
+
bucket = plan.proto_indices.get(proto)
|
|
171
|
+
if not isinstance(bucket, Mapping):
|
|
172
|
+
continue
|
|
173
|
+
exact_bucket = bucket.get("exact")
|
|
174
|
+
if not isinstance(exact_bucket, Mapping):
|
|
175
|
+
continue
|
|
176
|
+
for path, meta_index in exact_bucket.items():
|
|
177
|
+
if isinstance(path, str) and isinstance(meta_index, int):
|
|
178
|
+
exact[(proto, path)] = meta_index
|
|
179
|
+
cache[packed_id] = exact
|
|
180
|
+
return exact
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def resolve_program_id_from_exact_websocket(
|
|
184
|
+
plan: KernelPlan,
|
|
185
|
+
packed: PackedKernel,
|
|
186
|
+
protocol: str,
|
|
187
|
+
path: str,
|
|
188
|
+
cache: ExactWebSocketCache,
|
|
189
|
+
) -> int:
|
|
190
|
+
exact = resolve_hot_exact_websocket_routes(plan, packed, cache)
|
|
191
|
+
maybe = exact.get((protocol, path))
|
|
192
|
+
return maybe if isinstance(maybe, int) else -1
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def resolve_hot_exact_jsonrpc_routes(
|
|
196
|
+
plan: KernelPlan,
|
|
197
|
+
cache: ExactJsonRpcCache,
|
|
198
|
+
) -> Mapping[str, Mapping[str, tuple[int, str, str]]]:
|
|
199
|
+
plan_id = id(plan)
|
|
200
|
+
cached = cache.get(plan_id)
|
|
201
|
+
if cached is not None:
|
|
202
|
+
return cached
|
|
203
|
+
|
|
204
|
+
proto_indices = getattr(plan, "proto_indices", {}) or {}
|
|
205
|
+
exact_routes: dict[str, dict[str, tuple[int, str, str]]] = {}
|
|
206
|
+
if isinstance(proto_indices, Mapping):
|
|
207
|
+
for proto, bucket in proto_indices.items():
|
|
208
|
+
if not isinstance(proto, str) or not proto.endswith(".jsonrpc"):
|
|
209
|
+
continue
|
|
210
|
+
if not isinstance(bucket, Mapping):
|
|
211
|
+
continue
|
|
212
|
+
endpoints = bucket.get("endpoints")
|
|
213
|
+
if not isinstance(endpoints, Mapping):
|
|
214
|
+
continue
|
|
215
|
+
for endpoint, endpoint_bucket in endpoints.items():
|
|
216
|
+
if not isinstance(endpoint, str) or not endpoint:
|
|
217
|
+
continue
|
|
218
|
+
if not isinstance(endpoint_bucket, Mapping):
|
|
219
|
+
continue
|
|
220
|
+
method_map = exact_routes.setdefault(endpoint, {})
|
|
221
|
+
for rpc_method, entry in endpoint_bucket.items():
|
|
222
|
+
if not isinstance(rpc_method, str) or not rpc_method:
|
|
223
|
+
continue
|
|
224
|
+
if not isinstance(entry, Mapping):
|
|
225
|
+
continue
|
|
226
|
+
meta_index = entry.get("meta_index")
|
|
227
|
+
if not isinstance(meta_index, int):
|
|
228
|
+
continue
|
|
229
|
+
selector = str(entry.get("selector") or f"{endpoint}:{rpc_method}")
|
|
230
|
+
method_map[rpc_method] = (meta_index, str(proto), selector)
|
|
231
|
+
|
|
232
|
+
frozen = {endpoint: dict(method_map) for endpoint, method_map in exact_routes.items()}
|
|
233
|
+
cache[plan_id] = frozen
|
|
234
|
+
return frozen
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def normalize_jsonrpc_mount_path(path: str) -> str:
|
|
238
|
+
normalized = str(path or "").rstrip("/")
|
|
239
|
+
return normalized or "/"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def canonical_protocol_anchor_order(
|
|
8
|
+
binding: str,
|
|
9
|
+
subevent: str,
|
|
10
|
+
*,
|
|
11
|
+
edge: str = "ok",
|
|
12
|
+
) -> tuple[str, ...]:
|
|
13
|
+
del binding, subevent
|
|
14
|
+
order = [
|
|
15
|
+
"dispatch.exchange.select",
|
|
16
|
+
"dispatch.family.derive",
|
|
17
|
+
"dispatch.subevent.derive",
|
|
18
|
+
"framing.decode",
|
|
19
|
+
"framing.encode",
|
|
20
|
+
"transport.emit",
|
|
21
|
+
"transport.emit_complete",
|
|
22
|
+
]
|
|
23
|
+
if edge == "err":
|
|
24
|
+
order.extend(("err.ctx.build", "err.classify", "err.transport.shape"))
|
|
25
|
+
return tuple(order)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def python_protocol_anchor_trace(case: dict[str, Any]) -> tuple[str, ...]:
|
|
29
|
+
return canonical_protocol_anchor_order(
|
|
30
|
+
str(case.get("binding")),
|
|
31
|
+
str(case.get("subevent")),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rust_protocol_anchor_trace(case: dict[str, Any]) -> tuple[str, ...]:
|
|
36
|
+
del case
|
|
37
|
+
warnings.warn(
|
|
38
|
+
"rust_protocol_anchor_trace is deprecated; protocol anchors are Python-only.",
|
|
39
|
+
DeprecationWarning,
|
|
40
|
+
stacklevel=2,
|
|
41
|
+
)
|
|
42
|
+
raise RuntimeError("rust_protocol_anchor_trace is unavailable.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"canonical_protocol_anchor_order",
|
|
47
|
+
"python_protocol_anchor_trace",
|
|
48
|
+
"rust_protocol_anchor_trace",
|
|
49
|
+
]
|
|
@@ -11,6 +11,7 @@ from tigrbl_core._spec.binding_spec import (
|
|
|
11
11
|
webtransport_lane_for_profile,
|
|
12
12
|
webtransport_runtime_family,
|
|
13
13
|
)
|
|
14
|
+
from tigrbl_kernel.resume_policy import compile_resume_policy
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _unsupported(message: str) -> ValueError:
|
|
@@ -72,18 +73,30 @@ def compile_binding_protocol_plan(op_id: str, binding: Mapping[str, Any]) -> dic
|
|
|
72
73
|
{"family": "request", "subevent": "response.emit"},
|
|
73
74
|
)
|
|
74
75
|
elif kind in {"http.stream", "https.stream"}:
|
|
75
|
-
validate_binding_profile_exchange(
|
|
76
|
+
exchange = validate_binding_profile_exchange(
|
|
76
77
|
binding_kind=kind,
|
|
77
78
|
exchange=str(binding.get("exchange") or "server_stream"),
|
|
78
79
|
)
|
|
79
80
|
validate_app_framing_for_binding(binding_kind=kind, framing=str(framing or "stream"))
|
|
80
81
|
family = "stream"
|
|
81
82
|
framing = str(framing or "stream")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
if exchange == "client_stream":
|
|
84
|
+
anchors = (
|
|
85
|
+
"transport.receive",
|
|
86
|
+
"dispatch.subevent.derive",
|
|
87
|
+
"handler.invoke",
|
|
88
|
+
"transport.emit_complete",
|
|
89
|
+
)
|
|
90
|
+
rows = (
|
|
91
|
+
{"family": "stream", "subevent": "stream.chunk.received"},
|
|
92
|
+
{"family": "stream", "subevent": "stream.receive_complete"},
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
anchors = ("handler.invoke", "transport.emit", "transport.emit_complete")
|
|
96
|
+
rows = (
|
|
97
|
+
{"family": "stream", "subevent": "stream.chunk"},
|
|
98
|
+
{"family": "stream", "subevent": "stream.close"},
|
|
99
|
+
)
|
|
87
100
|
elif kind in {"http.sse", "https.sse"}:
|
|
88
101
|
validate_binding_profile_exchange(
|
|
89
102
|
binding_kind=kind,
|
|
@@ -239,8 +252,11 @@ def compile_binding_protocol_plan(op_id: str, binding: Mapping[str, Any]) -> dic
|
|
|
239
252
|
if kind == "webtransport":
|
|
240
253
|
event_key_inputs["lane"] = lane
|
|
241
254
|
event_key_inputs["inner_framing"] = inner_framing
|
|
255
|
+
resume_policy = compile_resume_policy(kind, binding)
|
|
256
|
+
if resume_policy.enabled:
|
|
257
|
+
event_key_inputs["resume_mode"] = resume_policy.mode
|
|
242
258
|
|
|
243
|
-
|
|
259
|
+
plan: dict[str, object] = {
|
|
244
260
|
"op_id": op_id,
|
|
245
261
|
"binding_kind": kind,
|
|
246
262
|
"family": family,
|
|
@@ -252,6 +268,9 @@ def compile_binding_protocol_plan(op_id: str, binding: Mapping[str, Any]) -> dic
|
|
|
252
268
|
},
|
|
253
269
|
"lifecycle_rows": rows,
|
|
254
270
|
}
|
|
271
|
+
if resume_policy.enabled:
|
|
272
|
+
plan["resume_policy"] = resume_policy.as_dict()
|
|
273
|
+
return plan
|
|
255
274
|
|
|
256
275
|
|
|
257
276
|
def _required_mask(*, kind: str, family: str, framing: str) -> int:
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
SERVER_STREAM_ANCHORS: tuple[str, ...] = (
|
|
7
7
|
"transport.ingress",
|
|
8
8
|
"binding.match",
|
|
9
9
|
"dispatch.exchange.select",
|
|
@@ -16,8 +16,41 @@ ANCHORS: tuple[str, ...] = (
|
|
|
16
16
|
"transport.emit_complete",
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
+
CLIENT_STREAM_ANCHORS: tuple[str, ...] = (
|
|
20
|
+
"transport.ingress",
|
|
21
|
+
"binding.match",
|
|
22
|
+
"dispatch.exchange.select",
|
|
23
|
+
"dispatch.family.derive",
|
|
24
|
+
"dispatch.subevent.derive",
|
|
25
|
+
"request.body.receive",
|
|
26
|
+
"stream.chunk.received",
|
|
27
|
+
"stream.receive_complete",
|
|
28
|
+
"operation.resolve",
|
|
29
|
+
"handler.call",
|
|
30
|
+
"transport.emit_complete",
|
|
31
|
+
)
|
|
32
|
+
|
|
19
33
|
|
|
20
34
|
def compile_http_stream_chain(binding: Mapping[str, Any]) -> dict[str, object]:
|
|
35
|
+
exchange = str(binding.get("exchange") or "server_stream")
|
|
36
|
+
if exchange == "client_stream":
|
|
37
|
+
consumer = binding.get("consumer", "request_body")
|
|
38
|
+
if consumer not in {"request_body", "async_iterator"}:
|
|
39
|
+
raise ValueError("consumer must be request_body or async_iterator for HTTP client stream")
|
|
40
|
+
return {
|
|
41
|
+
"binding": "http.stream",
|
|
42
|
+
"exchange": "client_stream",
|
|
43
|
+
"family": "stream",
|
|
44
|
+
"path": binding.get("path"),
|
|
45
|
+
"method": binding.get("method", "POST"),
|
|
46
|
+
"consumer": consumer,
|
|
47
|
+
"anchors": CLIENT_STREAM_ANCHORS,
|
|
48
|
+
"break_conditions": ("request.body_complete", "disconnect"),
|
|
49
|
+
"err_target": "transport.close",
|
|
50
|
+
"completion_fence": "POST_HANDLER",
|
|
51
|
+
}
|
|
52
|
+
if exchange != "server_stream":
|
|
53
|
+
raise ValueError("HTTP stream exchange must be server_stream or client_stream")
|
|
21
54
|
producer = binding.get("producer", "iterator")
|
|
22
55
|
if producer not in {"iterator", "async_iterator"}:
|
|
23
56
|
raise ValueError("producer must be iterator or async_iterator for HTTP stream")
|
|
@@ -28,7 +61,7 @@ def compile_http_stream_chain(binding: Mapping[str, Any]) -> dict[str, object]:
|
|
|
28
61
|
"path": binding.get("path"),
|
|
29
62
|
"method": binding.get("method", "GET"),
|
|
30
63
|
"producer": producer,
|
|
31
|
-
"anchors":
|
|
64
|
+
"anchors": SERVER_STREAM_ANCHORS,
|
|
32
65
|
"break_conditions": ("producer.exhausted", "disconnect"),
|
|
33
66
|
"err_target": "transport.close",
|
|
34
67
|
"completion_fence": "POST_EMIT",
|