paseo-plugin-zcode-provider 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/NOTICE.md +29 -0
- package/README.md +112 -2
- package/client/account.tsx +827 -0
- package/client/diagnostics.tsx +379 -0
- package/client/runtime.tsx +296 -0
- package/client/settings.tsx +84 -0
- package/client/value.tsx +20 -0
- package/icon.svg +1 -0
- package/index.client.tsx +22 -0
- package/index.server.ts +39 -0
- package/package.json +77 -4
- package/paseo-plugin.json +6 -0
- package/server/account.ts +781 -0
- package/server/attachments.ts +150 -0
- package/server/build-info.ts +2 -0
- package/server/conversation.ts +319 -0
- package/server/diagnostics.ts +252 -0
- package/server/discovery/discover.ts +225 -0
- package/server/discovery/manifest.ts +36 -0
- package/server/discovery/types.ts +30 -0
- package/server/errors.ts +36 -0
- package/server/host/bridge.ts +490 -0
- package/server/host/schemas.ts +119 -0
- package/server/logger.ts +19 -0
- package/server/mapping.ts +270 -0
- package/server/models.ts +68 -0
- package/server/persistence.ts +146 -0
- package/server/presentation.ts +470 -0
- package/server/provider-status.ts +81 -0
- package/server/provider.ts +838 -0
- package/server/runtime/managed.ts +501 -0
- package/server/runtime/pins.ts +72 -0
- package/server/runtime-setup.ts +77 -0
- package/server/session-types.ts +49 -0
- package/server/session.ts +866 -0
- package/server/status.ts +89 -0
- package/server/timeline.ts +18 -0
- package/server/usage.ts +362 -0
- package/server/vendor/zcode/LICENSE +202 -0
- package/server/vendor/zcode/NOTICE.md +70 -0
- package/server/vendor/zcode/THIRD-PARTY-NOTICES.md +37 -0
- package/server/vendor/zcode/packages/model-option-map/src/compiler.ts +92 -0
- package/server/vendor/zcode/packages/model-option-map/src/evaluator.ts +214 -0
- package/server/vendor/zcode/packages/model-option-map/src/index.ts +5 -0
- package/server/vendor/zcode/packages/model-option-map/src/merge-patch.ts +97 -0
- package/server/vendor/zcode/packages/model-option-map/src/option-maps.ts +42 -0
- package/server/vendor/zcode/packages/model-option-map/src/parser.ts +240 -0
- package/server/vendor/zcode/packages/model-option-map/src/tokenizer.ts +141 -0
- package/server/vendor/zcode/packages/model-option-map/src/types.ts +36 -0
- package/server/vendor/zcode/packages/rpc/src/buffer.ts +88 -0
- package/server/vendor/zcode/packages/rpc/src/channelClient.ts +275 -0
- package/server/vendor/zcode/packages/rpc/src/channelServer.ts +283 -0
- package/server/vendor/zcode/packages/rpc/src/channels.shared.ts +66 -0
- package/server/vendor/zcode/packages/rpc/src/channels.ts +4 -0
- package/server/vendor/zcode/packages/rpc/src/delayedChannel.ts +17 -0
- package/server/vendor/zcode/packages/rpc/src/foundation.ts +260 -0
- package/server/vendor/zcode/packages/rpc/src/protocol.ts +397 -0
- package/server/vendor/zcode/packages/rpc/src/proxy-channel.ts +213 -0
- package/server/vendor/zcode/packages/rpc/src/serialization.ts +291 -0
- package/server/vendor/zcode/packages/shared/src/account-provider-state.ts +13 -0
- package/server/vendor/zcode/packages/shared/src/background-bash-output.ts +24 -0
- package/server/vendor/zcode/packages/shared/src/background-task-controls.ts +404 -0
- package/server/vendor/zcode/packages/shared/src/bash-output-display.ts +11 -0
- package/server/vendor/zcode/packages/shared/src/bots.ts +558 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/backend.ts +80 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/command-metadata.ts +170 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/commands.ts +588 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/result.ts +130 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/snapshot.ts +77 -0
- package/server/vendor/zcode/packages/shared/src/command-types.ts +96 -0
- package/server/vendor/zcode/packages/shared/src/config-schema.ts +8 -0
- package/server/vendor/zcode/packages/shared/src/cuaAccessibilitySettings.ts +41 -0
- package/server/vendor/zcode/packages/shared/src/database-startup.ts +178 -0
- package/server/vendor/zcode/packages/shared/src/env.ts +63 -0
- package/server/vendor/zcode/packages/shared/src/execution-output-preview.ts +14 -0
- package/server/vendor/zcode/packages/shared/src/localTtft.ts +192 -0
- package/server/vendor/zcode/packages/shared/src/model-config.ts +123 -0
- package/server/vendor/zcode/packages/shared/src/model-execution.ts +23 -0
- package/server/vendor/zcode/packages/shared/src/model-provider-family.ts +171 -0
- package/server/vendor/zcode/packages/shared/src/model-provider-types.ts +81 -0
- package/server/vendor/zcode/packages/shared/src/model-selection.ts +60 -0
- package/server/vendor/zcode/packages/shared/src/oauth.ts +203 -0
- package/server/vendor/zcode/packages/shared/src/official-mcp-auth.ts +272 -0
- package/server/vendor/zcode/packages/shared/src/official-mcp-tool-error.ts +68 -0
- package/server/vendor/zcode/packages/shared/src/process-diagnostic.ts +42 -0
- package/server/vendor/zcode/packages/shared/src/protocol.ts +369 -0
- package/server/vendor/zcode/packages/shared/src/provider-family-connection-selection.ts +32 -0
- package/server/vendor/zcode/packages/shared/src/remoteAssetInstallMode.ts +11 -0
- package/server/vendor/zcode/packages/shared/src/remoteResourcePackages.ts +91 -0
- package/server/vendor/zcode/packages/shared/src/runtimeEnv.ts +315 -0
- package/server/vendor/zcode/packages/shared/src/settings-source.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/usage-quota.ts +74 -0
- package/server/vendor/zcode/packages/shared/src/usage-stats.ts +462 -0
- package/server/vendor/zcode/packages/shared/src/validationAppSettings.ts +562 -0
- package/server/vendor/zcode/packages/shared/src/workspacePurpose.ts +2 -0
- package/server/vendor/zcode/packages/shared/src/wslUserValidation.ts +33 -0
- package/server/vendor/zcode/packages/shared/src/zcode-agent-policy.ts +25 -0
- package/server/vendor/zcode/packages/shared/src/zcode-media-policy.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol/index.ts +3720 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-legacy-types.ts +529 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/apply.ts +218 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/attachment-ref.ts +14 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/command.ts +482 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/core.ts +100 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/create-workflow-display.ts +156 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/cuaPermission.ts +41 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/delta.ts +147 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/input-intent.ts +69 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/rows.ts +432 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/session-config.ts +43 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/sessions-index-workflow-activity.ts +195 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/sessions-index.ts +85 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/shared-context-import.ts +38 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/shared-context-ref.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/snapshot.ts +509 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/submission.ts +5 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/toolDisplay.ts +264 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/transport.ts +1152 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-assembler.ts +574 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-binary.ts +65 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-codec.ts +240 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-fault.ts +39 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire.ts +159 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-artifacts.ts +291 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-observation-display.ts +281 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-row-meta.ts +163 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-run-settings-command.ts +62 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-runs-delta.ts +517 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-runs.ts +512 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workspace-config.ts +81 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workspace-hook-review.ts +210 -0
- package/server/vendor/zcode/packages/shared/src/zcode-task-mode-schema.ts +6 -0
- package/server/vendor/zcode/packages/shared/src/zcode-task-types-core.ts +1187 -0
- package/server/vendor/zcode/packages/shared/src/zcodeEndpoint.ts +296 -0
- package/server/vendor/zcode/provenance.json +405 -0
- package/shared/account.ts +201 -0
- package/shared/diagnostics.ts +52 -0
- package/shared/runtime-setup.ts +73 -0
- package/vendor/acp-registry/LICENSE +176 -0
- package/vendor/paseo/LICENSE +211 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- physical assembly 的 ordinal、预算、校验与释放必须保持单一原子状态机。 */
|
|
2
|
+
// V4 incremental physical assembler:ownership 过滤后逐片接纳,只在
|
|
3
|
+
// checksum/UTF-8/JSON/schema 全部通过后产出一个 logical frame。
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { PROTOCOL_V4_LIMITS } from "./core.js";
|
|
6
|
+
import { crc32WireBytes, decodeWireBase64 } from "./wire-binary.js";
|
|
7
|
+
import { measureTopicNotificationEnvelopeBytes } from "./wire-codec.js";
|
|
8
|
+
import type { TopicFrameDeliveryKind, TopicWireFrameCandidate } from "./wire.js";
|
|
9
|
+
import { WIRE_FAULT_INVALID_PAYLOAD } from "./wire-fault.js";
|
|
10
|
+
|
|
11
|
+
export interface TopicWireAssemblyFault {
|
|
12
|
+
/** 缺失/伪值本身也必须成为 owned typed fault,此时不能伪造用途。 */
|
|
13
|
+
deliveryKind?: TopicFrameDeliveryKind;
|
|
14
|
+
reasonCode: string;
|
|
15
|
+
logicalFrameId: string;
|
|
16
|
+
logicalFrameOrdinal: number;
|
|
17
|
+
topic: string;
|
|
18
|
+
subscriptionId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TopicWireAssemblyEvent<F> =
|
|
22
|
+
| { kind: "complete"; frame: F; deliveryKind: TopicFrameDeliveryKind }
|
|
23
|
+
| { kind: "fault"; fault: TopicWireAssemblyFault };
|
|
24
|
+
|
|
25
|
+
export interface TopicWireFrameAssemblerOptions {
|
|
26
|
+
maxAssemblyBytes?: number;
|
|
27
|
+
maxFragments?: number;
|
|
28
|
+
maxConcurrentAssemblies?: number;
|
|
29
|
+
maxStagedDecodedBytes?: number;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
maxPhysicalFrameBytes?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FragmentAssembly {
|
|
35
|
+
deliveryKind: TopicFrameDeliveryKind;
|
|
36
|
+
logicalFrameId: string;
|
|
37
|
+
logicalFrameOrdinal: number;
|
|
38
|
+
topic: string;
|
|
39
|
+
subscriptionId: string;
|
|
40
|
+
fragmentCount: number;
|
|
41
|
+
logicalBytes: number;
|
|
42
|
+
checksum: { algorithm: "crc32"; value: string };
|
|
43
|
+
fragments: Array<Uint8Array | undefined>;
|
|
44
|
+
receivedCount: number;
|
|
45
|
+
decodedBytes: number;
|
|
46
|
+
firstSeenAt: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SettledLogicalFrame {
|
|
50
|
+
logicalFrameId: string;
|
|
51
|
+
logicalFrameOrdinal: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type FragmentWireCandidate = Extract<TopicWireFrameCandidate, { kind: "fragment" }>;
|
|
55
|
+
type ValidatedFragmentWire = FragmentWireCandidate & {
|
|
56
|
+
fragmentIndex: number;
|
|
57
|
+
fragmentCount: number;
|
|
58
|
+
logicalBytes: number;
|
|
59
|
+
checksum: { algorithm: string; value: string };
|
|
60
|
+
dataBase64: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const COMPLETE_WIRE_KEYS = new Set([
|
|
64
|
+
"wireVersion",
|
|
65
|
+
"kind",
|
|
66
|
+
"deliveryKind",
|
|
67
|
+
"logicalFrameId",
|
|
68
|
+
"logicalFrameOrdinal",
|
|
69
|
+
"topic",
|
|
70
|
+
"subscriptionId",
|
|
71
|
+
"frame",
|
|
72
|
+
]);
|
|
73
|
+
const FRAGMENT_WIRE_KEYS = new Set([
|
|
74
|
+
"wireVersion",
|
|
75
|
+
"kind",
|
|
76
|
+
"deliveryKind",
|
|
77
|
+
"logicalFrameId",
|
|
78
|
+
"logicalFrameOrdinal",
|
|
79
|
+
"topic",
|
|
80
|
+
"subscriptionId",
|
|
81
|
+
"fragmentIndex",
|
|
82
|
+
"fragmentCount",
|
|
83
|
+
"logicalBytes",
|
|
84
|
+
"checksum",
|
|
85
|
+
"dataBase64",
|
|
86
|
+
]);
|
|
87
|
+
const CHECKSUM_KEYS = new Set(["algorithm", "value"]);
|
|
88
|
+
|
|
89
|
+
function hasOnlyKeys(value: object, allowed: ReadonlySet<string>): boolean {
|
|
90
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseDeliveryKind(value: unknown): TopicFrameDeliveryKind | null {
|
|
94
|
+
return value === "initial" || value === "online" || value === "recovery" ? value : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasValidFragmentInner(wire: FragmentWireCandidate): wire is ValidatedFragmentWire {
|
|
98
|
+
if (
|
|
99
|
+
!hasOnlyKeys(wire, FRAGMENT_WIRE_KEYS) ||
|
|
100
|
+
typeof wire.fragmentIndex !== "number" ||
|
|
101
|
+
typeof wire.fragmentCount !== "number" ||
|
|
102
|
+
typeof wire.logicalBytes !== "number" ||
|
|
103
|
+
typeof wire.dataBase64 !== "string" ||
|
|
104
|
+
typeof wire.checksum !== "object" ||
|
|
105
|
+
wire.checksum === null ||
|
|
106
|
+
Array.isArray(wire.checksum) ||
|
|
107
|
+
!hasOnlyKeys(wire.checksum, CHECKSUM_KEYS)
|
|
108
|
+
) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const checksum = wire.checksum as Record<string, unknown>;
|
|
112
|
+
return typeof checksum.algorithm === "string" && typeof checksum.value === "string";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function routeKey(topic: string, subscriptionId: string): string {
|
|
116
|
+
return `${topic}\0${subscriptionId}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function bytesEqual(left: Uint8Array, right: Uint8Array): boolean {
|
|
120
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
121
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
122
|
+
if (left[index] !== right[index]) return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function frameMatchesEnvelope(
|
|
128
|
+
frame: unknown,
|
|
129
|
+
wire: { topic: string; subscriptionId: string },
|
|
130
|
+
): boolean {
|
|
131
|
+
if (typeof frame !== "object" || frame === null) return false;
|
|
132
|
+
const value = frame as { topic?: unknown; subscriptionId?: unknown };
|
|
133
|
+
return value.topic === wire.topic && value.subscriptionId === wire.subscriptionId;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hardBound(value: number | undefined, maximum: number, name: string): number {
|
|
137
|
+
const resolved = value ?? maximum;
|
|
138
|
+
if (!Number.isFinite(resolved) || resolved <= 0) {
|
|
139
|
+
throw new RangeError(`${name} must be a positive finite number`);
|
|
140
|
+
}
|
|
141
|
+
return Math.min(Math.floor(resolved), maximum);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class TopicWireFrameAssembler<F> {
|
|
145
|
+
private readonly assemblies = new Map<string, FragmentAssembly>();
|
|
146
|
+
/** 每 route 的单调 ordinal tombstone;不会像 bounded id LRU 一样淘汰后复活旧帧。 */
|
|
147
|
+
private readonly settledByRoute = new Map<string, SettledLogicalFrame>();
|
|
148
|
+
private stagedDecodedBytes = 0;
|
|
149
|
+
private readonly maxAssemblyBytes: number;
|
|
150
|
+
private readonly maxFragments: number;
|
|
151
|
+
private readonly maxConcurrentAssemblies: number;
|
|
152
|
+
private readonly maxStagedDecodedBytes: number;
|
|
153
|
+
private readonly timeoutMs: number;
|
|
154
|
+
private readonly maxPhysicalFrameBytes: number;
|
|
155
|
+
|
|
156
|
+
constructor(
|
|
157
|
+
private readonly frameSchema: z.ZodType<F>,
|
|
158
|
+
options: TopicWireFrameAssemblerOptions = {},
|
|
159
|
+
) {
|
|
160
|
+
this.maxAssemblyBytes = hardBound(
|
|
161
|
+
options.maxAssemblyBytes,
|
|
162
|
+
PROTOCOL_V4_LIMITS.logicalFrameAssemblyMaxBytes,
|
|
163
|
+
"maxAssemblyBytes",
|
|
164
|
+
);
|
|
165
|
+
this.maxFragments = hardBound(
|
|
166
|
+
options.maxFragments,
|
|
167
|
+
PROTOCOL_V4_LIMITS.logicalFrameAssemblyMaxFragments,
|
|
168
|
+
"maxFragments",
|
|
169
|
+
);
|
|
170
|
+
this.maxConcurrentAssemblies = hardBound(
|
|
171
|
+
options.maxConcurrentAssemblies,
|
|
172
|
+
PROTOCOL_V4_LIMITS.logicalFrameAssemblyMaxConcurrent,
|
|
173
|
+
"maxConcurrentAssemblies",
|
|
174
|
+
);
|
|
175
|
+
this.maxStagedDecodedBytes = hardBound(
|
|
176
|
+
options.maxStagedDecodedBytes,
|
|
177
|
+
PROTOCOL_V4_LIMITS.logicalFrameAssemblyMaxStagedBytes,
|
|
178
|
+
"maxStagedDecodedBytes",
|
|
179
|
+
);
|
|
180
|
+
this.timeoutMs = hardBound(
|
|
181
|
+
options.timeoutMs,
|
|
182
|
+
PROTOCOL_V4_LIMITS.logicalFrameAssemblyTimeoutMs,
|
|
183
|
+
"timeoutMs",
|
|
184
|
+
);
|
|
185
|
+
this.maxPhysicalFrameBytes = hardBound(
|
|
186
|
+
options.maxPhysicalFrameBytes,
|
|
187
|
+
PROTOCOL_V4_LIMITS.maxFrameBytes,
|
|
188
|
+
"maxPhysicalFrameBytes",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
accept(wire: TopicWireFrameCandidate, now = Date.now()): TopicWireAssemblyEvent<F>[] {
|
|
193
|
+
const events = this.expire(now);
|
|
194
|
+
const key = routeKey(wire.topic, wire.subscriptionId);
|
|
195
|
+
if (!Number.isSafeInteger(wire.logicalFrameOrdinal) || wire.logicalFrameOrdinal < 1) {
|
|
196
|
+
events.push(this.fault(wire, "proto.frameAssemblyMetadataMismatch"));
|
|
197
|
+
return events;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ordinal 淘汰检查必须早于 envelope/base64:迟到旧坏片不得释放正在组装的新帧。
|
|
201
|
+
const settled = this.settledByRoute.get(key);
|
|
202
|
+
if (settled) {
|
|
203
|
+
if (wire.logicalFrameOrdinal < settled.logicalFrameOrdinal) return events;
|
|
204
|
+
if (wire.logicalFrameOrdinal === settled.logicalFrameOrdinal) {
|
|
205
|
+
if (wire.logicalFrameId !== settled.logicalFrameId) {
|
|
206
|
+
events.push(this.fault(wire, "proto.frameAssemblyOrdinalConflict"));
|
|
207
|
+
}
|
|
208
|
+
return events;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const deliveryKind = parseDeliveryKind(wire.deliveryKind);
|
|
213
|
+
if (deliveryKind === "recovery") {
|
|
214
|
+
// accept() 先 expire 再接当前 wire。若旧 online 恰在 recovery 首片到达的
|
|
215
|
+
// 同次调用超时,向 consumer 上报旧 fault 会关掉 route 并丢失已接 recovery
|
|
216
|
+
// fragment。更高 ordinal 的权威 recovery 已取代旧 assembly,过滤该旧 fault;
|
|
217
|
+
// recovery 自身若坏,后续仍会产生带 recovery kind 的 typed fault。
|
|
218
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
219
|
+
const event = events[index];
|
|
220
|
+
if (
|
|
221
|
+
event?.kind === "fault" &&
|
|
222
|
+
event.fault.topic === wire.topic &&
|
|
223
|
+
event.fault.subscriptionId === wire.subscriptionId &&
|
|
224
|
+
event.fault.logicalFrameOrdinal < wire.logicalFrameOrdinal
|
|
225
|
+
) {
|
|
226
|
+
events.splice(index, 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const current = this.assemblies.get(key);
|
|
231
|
+
if (current) {
|
|
232
|
+
if (wire.logicalFrameOrdinal < current.logicalFrameOrdinal) return events;
|
|
233
|
+
if (wire.logicalFrameOrdinal === current.logicalFrameOrdinal) {
|
|
234
|
+
if (wire.logicalFrameId !== current.logicalFrameId) {
|
|
235
|
+
this.release(key, current);
|
|
236
|
+
this.settle(key, current);
|
|
237
|
+
events.push(this.fault(wire, "proto.frameAssemblyOrdinalConflict"));
|
|
238
|
+
return events;
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
this.release(key, current);
|
|
242
|
+
this.settle(key, current);
|
|
243
|
+
// same-sub recovery 本来就是权威替换残缺旧帧;若仍把旧 online
|
|
244
|
+
// assembly 作为同批 fault 上报,decoder 会 fail-close 并丢掉紧随其后的完整
|
|
245
|
+
// recovery,flight 永远无法收口。只有权威 recovery 可无 fault supersede。
|
|
246
|
+
if (deliveryKind !== "recovery") {
|
|
247
|
+
events.push(this.fault(current, "proto.frameAssemblySuperseded"));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (deliveryKind === null) {
|
|
253
|
+
this.releaseAndSettle(key, wire);
|
|
254
|
+
events.push(this.fault(wire, "proto.frameAssemblyMetadataMismatch"));
|
|
255
|
+
return events;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (wire.kind === "complete") {
|
|
259
|
+
if (
|
|
260
|
+
!hasOnlyKeys(wire, COMPLETE_WIRE_KEYS) ||
|
|
261
|
+
!Object.prototype.hasOwnProperty.call(wire, "frame")
|
|
262
|
+
) {
|
|
263
|
+
this.releaseAndSettle(key, wire);
|
|
264
|
+
events.push(this.fault(wire, "proto.frameAssemblyMetadataMismatch"));
|
|
265
|
+
return events;
|
|
266
|
+
}
|
|
267
|
+
if (measureTopicNotificationEnvelopeBytes(wire).maxBytes > this.maxPhysicalFrameBytes) {
|
|
268
|
+
this.releaseAndSettle(key, wire);
|
|
269
|
+
events.push(this.fault(wire, "proto.frameEnvelopeTooLarge"));
|
|
270
|
+
return events;
|
|
271
|
+
}
|
|
272
|
+
const active = this.assemblies.get(key);
|
|
273
|
+
if (active) {
|
|
274
|
+
this.release(key, active);
|
|
275
|
+
this.settle(key, active);
|
|
276
|
+
events.push(this.fault(active, "proto.frameAssemblyMetadataMismatch"));
|
|
277
|
+
return events;
|
|
278
|
+
}
|
|
279
|
+
const logicalBytes = new TextEncoder().encode(JSON.stringify(wire.frame)).byteLength;
|
|
280
|
+
if (logicalBytes > this.maxAssemblyBytes) {
|
|
281
|
+
this.settle(key, wire);
|
|
282
|
+
events.push(this.fault(wire, "proto.frameAssemblyTooLarge"));
|
|
283
|
+
return events;
|
|
284
|
+
}
|
|
285
|
+
if (!frameMatchesEnvelope(wire.frame, wire)) {
|
|
286
|
+
this.settle(key, wire);
|
|
287
|
+
events.push(this.fault(wire, "proto.frameAssemblyMetadataMismatch"));
|
|
288
|
+
return events;
|
|
289
|
+
}
|
|
290
|
+
const parsed = this.frameSchema.safeParse(wire.frame);
|
|
291
|
+
if (!parsed.success) {
|
|
292
|
+
this.settle(key, wire);
|
|
293
|
+
events.push(this.fault(wire, WIRE_FAULT_INVALID_PAYLOAD));
|
|
294
|
+
return events;
|
|
295
|
+
}
|
|
296
|
+
this.settle(key, wire);
|
|
297
|
+
events.push({ kind: "complete", frame: parsed.data, deliveryKind });
|
|
298
|
+
return events;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!hasValidFragmentInner(wire)) {
|
|
302
|
+
this.releaseAndSettle(key, wire);
|
|
303
|
+
events.push(this.fault(wire, "proto.frameAssemblyMetadataMismatch"));
|
|
304
|
+
return events;
|
|
305
|
+
}
|
|
306
|
+
if (measureTopicNotificationEnvelopeBytes(wire).maxBytes > this.maxPhysicalFrameBytes) {
|
|
307
|
+
this.releaseAndSettle(key, wire);
|
|
308
|
+
events.push(this.fault(wire, "proto.frameEnvelopeTooLarge"));
|
|
309
|
+
return events;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (wire.fragmentCount > this.maxFragments) {
|
|
313
|
+
this.releaseAndSettle(key, wire);
|
|
314
|
+
events.push(this.fault(wire, "proto.frameFragmentCountExceeded"));
|
|
315
|
+
return events;
|
|
316
|
+
}
|
|
317
|
+
if (wire.logicalBytes > this.maxAssemblyBytes) {
|
|
318
|
+
this.releaseAndSettle(key, wire);
|
|
319
|
+
events.push(this.fault(wire, "proto.frameAssemblyTooLarge"));
|
|
320
|
+
return events;
|
|
321
|
+
}
|
|
322
|
+
if (
|
|
323
|
+
!Number.isInteger(wire.fragmentCount) ||
|
|
324
|
+
wire.fragmentCount < 1 ||
|
|
325
|
+
!Number.isInteger(wire.fragmentIndex) ||
|
|
326
|
+
wire.fragmentIndex < 0 ||
|
|
327
|
+
wire.fragmentIndex >= wire.fragmentCount ||
|
|
328
|
+
!Number.isInteger(wire.logicalBytes) ||
|
|
329
|
+
wire.logicalBytes < 1 ||
|
|
330
|
+
wire.fragmentCount > wire.logicalBytes ||
|
|
331
|
+
wire.checksum.algorithm !== "crc32" ||
|
|
332
|
+
!/^[0-9a-f]{8}$/u.test(wire.checksum.value)
|
|
333
|
+
) {
|
|
334
|
+
this.releaseAndSettle(key, wire);
|
|
335
|
+
events.push(this.fault(wire, "proto.frameAssemblyMetadataMismatch"));
|
|
336
|
+
return events;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const decoded = decodeWireBase64(wire.dataBase64);
|
|
340
|
+
if (!decoded) {
|
|
341
|
+
this.releaseAndSettle(key, wire);
|
|
342
|
+
events.push(this.fault(wire, "proto.frameAssemblyInvalidBase64"));
|
|
343
|
+
return events;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let assembly = this.assemblies.get(key);
|
|
347
|
+
if (assembly) {
|
|
348
|
+
if (
|
|
349
|
+
assembly.fragmentCount !== wire.fragmentCount ||
|
|
350
|
+
assembly.deliveryKind !== deliveryKind ||
|
|
351
|
+
assembly.logicalBytes !== wire.logicalBytes ||
|
|
352
|
+
assembly.checksum.algorithm !== wire.checksum.algorithm ||
|
|
353
|
+
assembly.checksum.value !== wire.checksum.value
|
|
354
|
+
) {
|
|
355
|
+
this.release(key, assembly);
|
|
356
|
+
this.settle(key, assembly);
|
|
357
|
+
events.push(this.fault(wire, "proto.frameAssemblyMetadataMismatch"));
|
|
358
|
+
return events;
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
if (this.assemblies.size >= this.maxConcurrentAssemblies) {
|
|
362
|
+
this.settle(key, wire);
|
|
363
|
+
events.push(this.fault(wire, "proto.frameAssemblyConcurrentLimit"));
|
|
364
|
+
return events;
|
|
365
|
+
}
|
|
366
|
+
if (this.stagedDecodedBytes + decoded.byteLength > this.maxStagedDecodedBytes) {
|
|
367
|
+
this.settle(key, wire);
|
|
368
|
+
events.push(this.fault(wire, "proto.frameAssemblyBudgetExceeded"));
|
|
369
|
+
return events;
|
|
370
|
+
}
|
|
371
|
+
assembly = {
|
|
372
|
+
deliveryKind,
|
|
373
|
+
logicalFrameId: wire.logicalFrameId,
|
|
374
|
+
logicalFrameOrdinal: wire.logicalFrameOrdinal,
|
|
375
|
+
topic: wire.topic,
|
|
376
|
+
subscriptionId: wire.subscriptionId,
|
|
377
|
+
fragmentCount: wire.fragmentCount,
|
|
378
|
+
logicalBytes: wire.logicalBytes,
|
|
379
|
+
checksum: { algorithm: "crc32", value: wire.checksum.value },
|
|
380
|
+
fragments: Array.from(
|
|
381
|
+
{ length: wire.fragmentCount },
|
|
382
|
+
() => undefined as Uint8Array | undefined,
|
|
383
|
+
),
|
|
384
|
+
receivedCount: 0,
|
|
385
|
+
decodedBytes: 0,
|
|
386
|
+
firstSeenAt: now,
|
|
387
|
+
};
|
|
388
|
+
this.assemblies.set(key, assembly);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const previous = assembly.fragments[wire.fragmentIndex];
|
|
392
|
+
if (previous) {
|
|
393
|
+
if (!bytesEqual(previous, decoded)) {
|
|
394
|
+
this.release(key, assembly);
|
|
395
|
+
this.settle(key, assembly);
|
|
396
|
+
events.push(this.fault(wire, "proto.frameAssemblyFragmentConflict"));
|
|
397
|
+
}
|
|
398
|
+
return events;
|
|
399
|
+
}
|
|
400
|
+
if (this.stagedDecodedBytes + decoded.byteLength > this.maxStagedDecodedBytes) {
|
|
401
|
+
this.release(key, assembly);
|
|
402
|
+
this.settle(key, assembly);
|
|
403
|
+
events.push(this.fault(wire, "proto.frameAssemblyBudgetExceeded"));
|
|
404
|
+
return events;
|
|
405
|
+
}
|
|
406
|
+
if (assembly.decodedBytes + decoded.byteLength > assembly.logicalBytes) {
|
|
407
|
+
this.release(key, assembly);
|
|
408
|
+
this.settle(key, assembly);
|
|
409
|
+
events.push(this.fault(wire, "proto.frameAssemblyLengthMismatch"));
|
|
410
|
+
return events;
|
|
411
|
+
}
|
|
412
|
+
assembly.fragments[wire.fragmentIndex] = decoded;
|
|
413
|
+
assembly.receivedCount += 1;
|
|
414
|
+
assembly.decodedBytes += decoded.byteLength;
|
|
415
|
+
this.stagedDecodedBytes += decoded.byteLength;
|
|
416
|
+
if (assembly.receivedCount !== assembly.fragmentCount) return events;
|
|
417
|
+
|
|
418
|
+
this.release(key, assembly);
|
|
419
|
+
if (assembly.decodedBytes !== assembly.logicalBytes) {
|
|
420
|
+
this.settle(key, assembly);
|
|
421
|
+
events.push(this.fault(assembly, "proto.frameAssemblyLengthMismatch"));
|
|
422
|
+
return events;
|
|
423
|
+
}
|
|
424
|
+
const logical = new Uint8Array(assembly.decodedBytes);
|
|
425
|
+
let offset = 0;
|
|
426
|
+
for (const fragment of assembly.fragments) {
|
|
427
|
+
if (!fragment) {
|
|
428
|
+
this.settle(key, assembly);
|
|
429
|
+
events.push(this.fault(assembly, "proto.frameAssemblyLengthMismatch"));
|
|
430
|
+
return events;
|
|
431
|
+
}
|
|
432
|
+
logical.set(fragment, offset);
|
|
433
|
+
offset += fragment.byteLength;
|
|
434
|
+
}
|
|
435
|
+
if (crc32WireBytes(logical) !== assembly.checksum.value) {
|
|
436
|
+
this.settle(key, assembly);
|
|
437
|
+
events.push(this.fault(assembly, "proto.frameAssemblyChecksumMismatch"));
|
|
438
|
+
return events;
|
|
439
|
+
}
|
|
440
|
+
let json: string;
|
|
441
|
+
try {
|
|
442
|
+
json = new TextDecoder("utf-8", { fatal: true }).decode(logical);
|
|
443
|
+
} catch {
|
|
444
|
+
this.settle(key, assembly);
|
|
445
|
+
events.push(this.fault(assembly, "proto.frameAssemblyInvalidUtf8"));
|
|
446
|
+
return events;
|
|
447
|
+
}
|
|
448
|
+
let value: unknown;
|
|
449
|
+
try {
|
|
450
|
+
value = JSON.parse(json);
|
|
451
|
+
} catch {
|
|
452
|
+
this.settle(key, assembly);
|
|
453
|
+
events.push(this.fault(assembly, "proto.frameAssemblyInvalidJson"));
|
|
454
|
+
return events;
|
|
455
|
+
}
|
|
456
|
+
if (!frameMatchesEnvelope(value, assembly)) {
|
|
457
|
+
this.settle(key, assembly);
|
|
458
|
+
events.push(this.fault(assembly, "proto.frameAssemblyMetadataMismatch"));
|
|
459
|
+
return events;
|
|
460
|
+
}
|
|
461
|
+
const parsed = this.frameSchema.safeParse(value);
|
|
462
|
+
if (!parsed.success) {
|
|
463
|
+
this.settle(key, assembly);
|
|
464
|
+
events.push(this.fault(assembly, WIRE_FAULT_INVALID_PAYLOAD));
|
|
465
|
+
return events;
|
|
466
|
+
}
|
|
467
|
+
this.settle(key, assembly);
|
|
468
|
+
events.push({ kind: "complete", frame: parsed.data, deliveryKind: assembly.deliveryKind });
|
|
469
|
+
return events;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
expire(now = Date.now()): TopicWireAssemblyEvent<F>[] {
|
|
473
|
+
const events: TopicWireAssemblyEvent<F>[] = [];
|
|
474
|
+
for (const [key, assembly] of this.assemblies) {
|
|
475
|
+
if (now - assembly.firstSeenAt < this.timeoutMs) continue;
|
|
476
|
+
this.release(key, assembly);
|
|
477
|
+
this.settle(key, assembly);
|
|
478
|
+
events.push(this.fault(assembly, "proto.frameAssemblyTimedOut"));
|
|
479
|
+
}
|
|
480
|
+
return events;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
discard(topic: string, subscriptionId: string): void {
|
|
484
|
+
const key = routeKey(topic, subscriptionId);
|
|
485
|
+
const assembly = this.assemblies.get(key);
|
|
486
|
+
if (assembly) this.release(key, assembly);
|
|
487
|
+
this.settledByRoute.delete(key);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** fault 后释放该 route 的 active bytes,但保留 ordinal tombstone 防旧 replay 复活。 */
|
|
491
|
+
abort(topic: string, subscriptionId: string): void {
|
|
492
|
+
const key = routeKey(topic, subscriptionId);
|
|
493
|
+
const assembly = this.assemblies.get(key);
|
|
494
|
+
if (!assembly) return;
|
|
495
|
+
this.release(key, assembly);
|
|
496
|
+
this.settle(key, assembly);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
clear(): void {
|
|
500
|
+
this.assemblies.clear();
|
|
501
|
+
this.settledByRoute.clear();
|
|
502
|
+
this.stagedDecodedBytes = 0;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
getStats(): { assemblies: number; stagedDecodedBytes: number } {
|
|
506
|
+
return { assemblies: this.assemblies.size, stagedDecodedBytes: this.stagedDecodedBytes };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
get nextExpiryAt(): number | null {
|
|
510
|
+
let next: number | null = null;
|
|
511
|
+
for (const assembly of this.assemblies.values()) {
|
|
512
|
+
const expiresAt = assembly.firstSeenAt + this.timeoutMs;
|
|
513
|
+
if (next === null || expiresAt < next) next = expiresAt;
|
|
514
|
+
}
|
|
515
|
+
return next;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private release(key: string, assembly: FragmentAssembly): void {
|
|
519
|
+
if (this.assemblies.get(key) !== assembly) return;
|
|
520
|
+
this.assemblies.delete(key);
|
|
521
|
+
this.stagedDecodedBytes -= assembly.decodedBytes;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private settle(
|
|
525
|
+
key: string,
|
|
526
|
+
frame: Pick<FragmentAssembly, "logicalFrameId" | "logicalFrameOrdinal">,
|
|
527
|
+
): void {
|
|
528
|
+
const previous = this.settledByRoute.get(key);
|
|
529
|
+
if (previous && previous.logicalFrameOrdinal > frame.logicalFrameOrdinal) return;
|
|
530
|
+
this.settledByRoute.set(key, {
|
|
531
|
+
logicalFrameId: frame.logicalFrameId,
|
|
532
|
+
logicalFrameOrdinal: frame.logicalFrameOrdinal,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private releaseAndSettle(
|
|
537
|
+
key: string,
|
|
538
|
+
frame: Pick<FragmentAssembly, "logicalFrameId" | "logicalFrameOrdinal">,
|
|
539
|
+
): void {
|
|
540
|
+
const active = this.assemblies.get(key);
|
|
541
|
+
if (
|
|
542
|
+
active &&
|
|
543
|
+
active.logicalFrameOrdinal === frame.logicalFrameOrdinal &&
|
|
544
|
+
active.logicalFrameId === frame.logicalFrameId
|
|
545
|
+
) {
|
|
546
|
+
this.release(key, active);
|
|
547
|
+
}
|
|
548
|
+
this.settle(key, frame);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private fault(
|
|
552
|
+
source: {
|
|
553
|
+
deliveryKind?: unknown;
|
|
554
|
+
logicalFrameId: string;
|
|
555
|
+
logicalFrameOrdinal: number;
|
|
556
|
+
topic: string;
|
|
557
|
+
subscriptionId: string;
|
|
558
|
+
},
|
|
559
|
+
reasonCode: string,
|
|
560
|
+
): TopicWireAssemblyEvent<F> {
|
|
561
|
+
const deliveryKind = parseDeliveryKind(source.deliveryKind);
|
|
562
|
+
return {
|
|
563
|
+
kind: "fault",
|
|
564
|
+
fault: {
|
|
565
|
+
...(deliveryKind === null ? {} : { deliveryKind }),
|
|
566
|
+
reasonCode,
|
|
567
|
+
logicalFrameId: source.logicalFrameId,
|
|
568
|
+
logicalFrameOrdinal: source.logicalFrameOrdinal,
|
|
569
|
+
topic: source.topic,
|
|
570
|
+
subscriptionId: source.subscriptionId,
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// V4 wire 的跨 Node/browser 二进制纯函数。独立成小模块,避免 codec 主文件
|
|
2
|
+
// 同时承载 schema、分片状态机和编码细节。
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
export const topicWireBase64Schema = z
|
|
6
|
+
.string()
|
|
7
|
+
.min(4)
|
|
8
|
+
.regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u);
|
|
9
|
+
|
|
10
|
+
export function crc32WireBytes(bytes: Uint8Array): string {
|
|
11
|
+
let crc = 0xffffffff;
|
|
12
|
+
for (const byte of bytes) {
|
|
13
|
+
crc ^= byte;
|
|
14
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
15
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return ((crc ^ 0xffffffff) >>> 0).toString(16).padStart(8, "0");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
22
|
+
|
|
23
|
+
export function encodeWireBytesBase64(bytes: Uint8Array): string {
|
|
24
|
+
let result = "";
|
|
25
|
+
let block: string[] = [];
|
|
26
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 3) {
|
|
27
|
+
const a = bytes[offset] ?? 0;
|
|
28
|
+
const hasB = offset + 1 < bytes.byteLength;
|
|
29
|
+
const hasC = offset + 2 < bytes.byteLength;
|
|
30
|
+
const b = hasB ? (bytes[offset + 1] ?? 0) : 0;
|
|
31
|
+
const c = hasC ? (bytes[offset + 2] ?? 0) : 0;
|
|
32
|
+
block.push(
|
|
33
|
+
BASE64_ALPHABET[a >>> 2]!,
|
|
34
|
+
BASE64_ALPHABET[((a & 0x03) << 4) | (b >>> 4)]!,
|
|
35
|
+
hasB ? BASE64_ALPHABET[((b & 0x0f) << 2) | (c >>> 6)]! : "=",
|
|
36
|
+
hasC ? BASE64_ALPHABET[c & 0x3f]! : "=",
|
|
37
|
+
);
|
|
38
|
+
if (block.length >= 16_384) {
|
|
39
|
+
result += block.join("");
|
|
40
|
+
block = [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result + block.join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function decodeWireBase64(value: string): Uint8Array | null {
|
|
47
|
+
if (!topicWireBase64Schema.safeParse(value).success) return null;
|
|
48
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
49
|
+
const output = new Uint8Array((value.length / 4) * 3 - padding);
|
|
50
|
+
let writeOffset = 0;
|
|
51
|
+
for (let offset = 0; offset < value.length; offset += 4) {
|
|
52
|
+
const a = BASE64_ALPHABET.indexOf(value[offset]!);
|
|
53
|
+
const b = BASE64_ALPHABET.indexOf(value[offset + 1]!);
|
|
54
|
+
const c = value[offset + 2] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[offset + 2]!);
|
|
55
|
+
const d = value[offset + 3] === "=" ? 0 : BASE64_ALPHABET.indexOf(value[offset + 3]!);
|
|
56
|
+
if (a < 0 || b < 0 || c < 0 || d < 0) return null;
|
|
57
|
+
const combined = (a << 18) | (b << 12) | (c << 6) | d;
|
|
58
|
+
if (writeOffset < output.length) output[writeOffset++] = combined >>> 16;
|
|
59
|
+
if (writeOffset < output.length) {
|
|
60
|
+
output[writeOffset++] = (combined >>> 8) & 0xff;
|
|
61
|
+
}
|
|
62
|
+
if (writeOffset < output.length) output[writeOffset++] = combined & 0xff;
|
|
63
|
+
}
|
|
64
|
+
return output;
|
|
65
|
+
}
|