tigrbl-kernel 0.4.2.dev4__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.
@@ -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
- anchors = ("handler.invoke", "transport.emit", "transport.emit_complete")
83
- rows = (
84
- {"family": "stream", "subevent": "stream.chunk"},
85
- {"family": "stream", "subevent": "stream.close"},
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
- return {
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
- ANCHORS: tuple[str, ...] = (
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": 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",