@query-farm/vgi 0.6.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 +134 -0
- package/README.md +494 -0
- package/dist/__dbg.test-d.d.ts +1 -0
- package/dist/arguments/argument-spec.d.ts +51 -0
- package/dist/arguments/arguments.d.ts +13 -0
- package/dist/arrow/codec/branded.d.ts +83 -0
- package/dist/arrow/codec/registry.d.ts +29 -0
- package/dist/arrow/codec/repr.d.ts +23 -0
- package/dist/arrow/codec/type-descriptors.d.ts +109 -0
- package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
- package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
- package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
- package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
- package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
- package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
- package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
- package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
- package/dist/arrow/impl-flechette/build.d.ts +14 -0
- package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
- package/dist/arrow/impl-flechette/empty.d.ts +9 -0
- package/dist/arrow/impl-flechette/filter.d.ts +10 -0
- package/dist/arrow/impl-flechette/index.d.ts +11 -0
- package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
- package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
- package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
- package/dist/arrow/impl-flechette/project.d.ts +3 -0
- package/dist/arrow/impl-flechette/schema.d.ts +47 -0
- package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
- package/dist/arrow/index.d.ts +11 -0
- package/dist/arrow/predicates.d.ts +47 -0
- package/dist/arrow/repr.test-d.d.ts +52 -0
- package/dist/arrow/schema-types.d.ts +71 -0
- package/dist/arrow/types.d.ts +57 -0
- package/dist/catalog/attach-option.d.ts +33 -0
- package/dist/catalog/attach-options.d.ts +30 -0
- package/dist/catalog/composite.d.ts +33 -0
- package/dist/catalog/descriptors.d.ts +154 -0
- package/dist/catalog/interface.d.ts +239 -0
- package/dist/catalog/read-only.d.ts +33 -0
- package/dist/client/client.d.ts +189 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/helpers.d.ts +12 -0
- package/dist/client/protocol.d.ts +16 -0
- package/dist/client/types.d.ts +125 -0
- package/dist/client-entry.d.ts +11 -0
- package/dist/client-entry.js +48 -0
- package/dist/client-entry.js.map +9 -0
- package/dist/codec/asd.d.ts +25 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/errors.d.ts +24 -0
- package/dist/filter-pushdown/collector.d.ts +32 -0
- package/dist/filter-pushdown/deserialize.d.ts +16 -0
- package/dist/filter-pushdown/evaluate.d.ts +51 -0
- package/dist/filter-pushdown/index.d.ts +4 -0
- package/dist/filter-pushdown/types.d.ts +84 -0
- package/dist/functions/aggregate.d.ts +129 -0
- package/dist/functions/copy-from.d.ts +81 -0
- package/dist/functions/copy-to.d.ts +121 -0
- package/dist/functions/registry.d.ts +16 -0
- package/dist/functions/scalar.d.ts +98 -0
- package/dist/functions/storage-cf-do.d.ts +47 -0
- package/dist/functions/storage.d.ts +86 -0
- package/dist/functions/table-buffering.d.ts +99 -0
- package/dist/functions/table-in-out.d.ts +64 -0
- package/dist/functions/table.d.ts +118 -0
- package/dist/functions/types.d.ts +157 -0
- package/dist/generated/vgi-client.d.ts +466 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
- package/dist/index.core.d.ts +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10888 -0
- package/dist/index.js.map +85 -0
- package/dist/metadata/resolve.d.ts +3 -0
- package/dist/metadata/serialize.d.ts +12 -0
- package/dist/metadata/types.d.ts +47 -0
- package/dist/protocol/dispatch.d.ts +23 -0
- package/dist/protocol/handlers/aggregate.d.ts +3 -0
- package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
- package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
- package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
- package/dist/protocol/handlers/catalog/table.d.ts +3 -0
- package/dist/protocol/handlers/catalog/view.d.ts +3 -0
- package/dist/protocol/handlers/function.d.ts +8 -0
- package/dist/protocol/handlers/shared.d.ts +31 -0
- package/dist/protocol/handlers/table-buffering.d.ts +3 -0
- package/dist/protocol/serialize.d.ts +4 -0
- package/dist/protocol/serializers/arguments.d.ts +3 -0
- package/dist/protocol/serializers/bind.d.ts +9 -0
- package/dist/protocol/serializers/cardinality.d.ts +7 -0
- package/dist/protocol/serializers/init.d.ts +9 -0
- package/dist/protocol/serializers/shared.d.ts +12 -0
- package/dist/protocol/state-serializer.d.ts +23 -0
- package/dist/protocol/types.d.ts +129 -0
- package/dist/secrets/helpers.d.ts +15 -0
- package/dist/types.d.ts +52 -0
- package/dist/util/arrow/index.d.ts +1 -0
- package/dist/util/bytes.d.ts +2 -0
- package/dist/util/statistics.d.ts +7 -0
- package/dist/worker-cf-entry.d.ts +30 -0
- package/dist/worker-cf-entry.js +8767 -0
- package/dist/worker-cf-entry.js.map +74 -0
- package/dist/worker.d.ts +36 -0
- package/package.json +96 -0
- package/src/arguments/argument-spec.ts +200 -0
- package/src/arguments/arguments.ts +81 -0
- package/src/arrow/codec/branded.ts +206 -0
- package/src/arrow/codec/registry.ts +674 -0
- package/src/arrow/codec/repr.ts +181 -0
- package/src/arrow/codec/type-descriptors.ts +137 -0
- package/src/arrow/impl-arrowjs/build.ts +88 -0
- package/src/arrow/impl-arrowjs/canonical.ts +509 -0
- package/src/arrow/impl-arrowjs/empty.ts +72 -0
- package/src/arrow/impl-arrowjs/filter.ts +53 -0
- package/src/arrow/impl-arrowjs/index.ts +54 -0
- package/src/arrow/impl-arrowjs/ipc.ts +67 -0
- package/src/arrow/impl-arrowjs/iterate.ts +159 -0
- package/src/arrow/impl-arrowjs/project.ts +73 -0
- package/src/arrow/impl-arrowjs/schema.ts +256 -0
- package/src/arrow/impl-arrowjs/statistics.ts +185 -0
- package/src/arrow/impl-flechette/build.ts +65 -0
- package/src/arrow/impl-flechette/canonical.ts +289 -0
- package/src/arrow/impl-flechette/empty.ts +20 -0
- package/src/arrow/impl-flechette/filter.ts +42 -0
- package/src/arrow/impl-flechette/index.ts +54 -0
- package/src/arrow/impl-flechette/ipc.ts +110 -0
- package/src/arrow/impl-flechette/iterate.ts +121 -0
- package/src/arrow/impl-flechette/message-meta.ts +175 -0
- package/src/arrow/impl-flechette/normalize-type.ts +99 -0
- package/src/arrow/impl-flechette/project.ts +64 -0
- package/src/arrow/impl-flechette/schema.ts +202 -0
- package/src/arrow/impl-flechette/statistics.ts +198 -0
- package/src/arrow/index.ts +129 -0
- package/src/arrow/predicates.ts +66 -0
- package/src/arrow/repr.test-d.ts +189 -0
- package/src/arrow/schema-types.ts +213 -0
- package/src/arrow/types.ts +74 -0
- package/src/catalog/attach-option.ts +107 -0
- package/src/catalog/attach-options.ts +79 -0
- package/src/catalog/composite.ts +180 -0
- package/src/catalog/descriptors.ts +164 -0
- package/src/catalog/interface.ts +721 -0
- package/src/catalog/read-only.ts +909 -0
- package/src/client/client.ts +1348 -0
- package/src/client/errors.ts +69 -0
- package/src/client/helpers.ts +56 -0
- package/src/client/protocol.ts +33 -0
- package/src/client/types.ts +140 -0
- package/src/client-entry.ts +112 -0
- package/src/codec/asd.ts +147 -0
- package/src/crypto.ts +143 -0
- package/src/errors.ts +61 -0
- package/src/filter-pushdown/collector.ts +147 -0
- package/src/filter-pushdown/deserialize.ts +253 -0
- package/src/filter-pushdown/evaluate.ts +591 -0
- package/src/filter-pushdown/index.ts +28 -0
- package/src/filter-pushdown/types.ts +91 -0
- package/src/functions/aggregate.ts +384 -0
- package/src/functions/copy-from.ts +282 -0
- package/src/functions/copy-to.ts +319 -0
- package/src/functions/registry.ts +258 -0
- package/src/functions/scalar.ts +356 -0
- package/src/functions/storage-cf-do.ts +303 -0
- package/src/functions/storage.ts +521 -0
- package/src/functions/table-buffering.ts +562 -0
- package/src/functions/table-in-out.ts +373 -0
- package/src/functions/table.ts +521 -0
- package/src/functions/types.ts +192 -0
- package/src/generated/vgi-client.ts +560 -0
- package/src/generated/vgi-protocol-schemas.ts +864 -0
- package/src/index.core.ts +267 -0
- package/src/index.ts +11 -0
- package/src/metadata/resolve.ts +109 -0
- package/src/metadata/serialize.ts +242 -0
- package/src/metadata/types.ts +60 -0
- package/src/protocol/dispatch.ts +52 -0
- package/src/protocol/handlers/aggregate.ts +355 -0
- package/src/protocol/handlers/catalog/admin.ts +368 -0
- package/src/protocol/handlers/catalog/index.ts +25 -0
- package/src/protocol/handlers/catalog/index_methods.ts +65 -0
- package/src/protocol/handlers/catalog/macro.ts +114 -0
- package/src/protocol/handlers/catalog/shared.ts +291 -0
- package/src/protocol/handlers/catalog/table.ts +401 -0
- package/src/protocol/handlers/catalog/view.ts +119 -0
- package/src/protocol/handlers/function.ts +302 -0
- package/src/protocol/handlers/shared.ts +80 -0
- package/src/protocol/handlers/table-buffering.ts +207 -0
- package/src/protocol/serialize.ts +23 -0
- package/src/protocol/serializers/arguments.ts +160 -0
- package/src/protocol/serializers/bind.ts +212 -0
- package/src/protocol/serializers/cardinality.ts +45 -0
- package/src/protocol/serializers/init.ts +247 -0
- package/src/protocol/serializers/shared.ts +46 -0
- package/src/protocol/state-serializer.ts +181 -0
- package/src/protocol/types.ts +153 -0
- package/src/secrets/helpers.ts +70 -0
- package/src/types.ts +71 -0
- package/src/util/arrow/index.ts +23 -0
- package/src/util/bytes.ts +14 -0
- package/src/util/statistics.ts +40 -0
- package/src/worker-cf-entry.ts +172 -0
- package/src/worker.ts +212 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Scalar function implementation.
|
|
3
|
+
// Scalar functions receive columnar input and produce a single output column
|
|
4
|
+
// with 1:1 row mapping.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type VgiSchema,
|
|
8
|
+
type VgiField,
|
|
9
|
+
type VgiBatch,
|
|
10
|
+
type VgiDataType,
|
|
11
|
+
type ValueFor,
|
|
12
|
+
type Repr,
|
|
13
|
+
schema as makeSchema,
|
|
14
|
+
field,
|
|
15
|
+
nullType,
|
|
16
|
+
isNull,
|
|
17
|
+
isBatch,
|
|
18
|
+
} from "../arrow/index.js";
|
|
19
|
+
import type { OutputCollector, AuthContext } from "@query-farm/vgi-rpc";
|
|
20
|
+
import {
|
|
21
|
+
FunctionType,
|
|
22
|
+
FunctionStability,
|
|
23
|
+
NullHandling,
|
|
24
|
+
DEFAULT_MAX_WORKERS,
|
|
25
|
+
} from "../types.js";
|
|
26
|
+
import { RowCountMismatchError } from "../errors.js";
|
|
27
|
+
import type {
|
|
28
|
+
BindRequest,
|
|
29
|
+
BindResponse,
|
|
30
|
+
InitRequest,
|
|
31
|
+
GlobalInitResponse,
|
|
32
|
+
} from "../protocol/types.js";
|
|
33
|
+
import type {
|
|
34
|
+
VgiFunction,
|
|
35
|
+
FunctionMeta,
|
|
36
|
+
StreamHandlers,
|
|
37
|
+
FunctionExample,
|
|
38
|
+
} from "./types.js";
|
|
39
|
+
import type { ArgumentSpec } from "../arguments/argument-spec.js";
|
|
40
|
+
import { argumentSpecsToSchema } from "../arguments/argument-spec.js";
|
|
41
|
+
import { batchToScalarDict, batchToSecretDict, batchFromColumns, projectBatch, safeNumber } from "../util/arrow/index.js";
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Scalar Bind Parameters
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
export interface ScalarBindParameters {
|
|
48
|
+
/** Constant argument values (resolved at bind time) */
|
|
49
|
+
constArgs: Record<string, any>;
|
|
50
|
+
/** Schema of all arguments including columnar params */
|
|
51
|
+
argumentsSchema: VgiSchema;
|
|
52
|
+
/** Settings from DuckDB */
|
|
53
|
+
settings: Record<string, any>;
|
|
54
|
+
/** Secrets from DuckDB */
|
|
55
|
+
secrets: Record<string, Record<string, any>>;
|
|
56
|
+
/** Original bind request — exposes attach_opaque_data, transaction_opaque_data, function_type, etc. */
|
|
57
|
+
bindCall: BindRequest;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Functional API
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
/** Ordered parameter definition for scalar functions. */
|
|
65
|
+
export interface ScalarParameterDef {
|
|
66
|
+
name: string;
|
|
67
|
+
type: VgiDataType;
|
|
68
|
+
/** If true, this is a constant parameter (scalar value resolved at bind time). */
|
|
69
|
+
const?: boolean;
|
|
70
|
+
/** If true, this parameter accepts variable number of arguments. */
|
|
71
|
+
varargs?: boolean;
|
|
72
|
+
/** Human-readable per-argument description (surfaced as `vgi_doc`). */
|
|
73
|
+
doc?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The typed compute row for a scalar function: one property per declared
|
|
78
|
+
* columnar param, valued in the representation (`rich` / `raw`) selected by the
|
|
79
|
+
* function's `repr`. (Provided as a convenience type for authors who build rows
|
|
80
|
+
* out of the input batch.)
|
|
81
|
+
*/
|
|
82
|
+
export type ScalarComputeRow<
|
|
83
|
+
P extends Record<string, VgiDataType>,
|
|
84
|
+
M extends Repr,
|
|
85
|
+
> = { [K in keyof P]: ValueFor<P[K], M> | null };
|
|
86
|
+
|
|
87
|
+
/** A single output value for a scalar function under representation `M`. */
|
|
88
|
+
export type ScalarOutputValue<R extends VgiDataType, M extends Repr> =
|
|
89
|
+
ValueFor<R, M> | null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Accepted compute return shapes: an array (or iterable) of output values in
|
|
93
|
+
* the declared representation, or a pre-built VgiBatch. The representation is
|
|
94
|
+
* enforced statically — returning a `Date` under `repr: 'raw'` (which expects a
|
|
95
|
+
* branded `Date32`/`Date64Ms`), or a branded value under `repr: 'rich'` (which
|
|
96
|
+
* expects a `Date`), is a COMPILE error.
|
|
97
|
+
*/
|
|
98
|
+
export type ScalarComputeResult<R extends VgiDataType, M extends Repr> =
|
|
99
|
+
| Array<ScalarOutputValue<R, M>>
|
|
100
|
+
| Iterable<ScalarOutputValue<R, M>>
|
|
101
|
+
| VgiBatch;
|
|
102
|
+
|
|
103
|
+
export interface ScalarFunctionConfig<
|
|
104
|
+
P extends Record<string, VgiDataType> = Record<string, VgiDataType>,
|
|
105
|
+
R extends VgiDataType = VgiDataType,
|
|
106
|
+
M extends Repr = "rich",
|
|
107
|
+
> {
|
|
108
|
+
name: string;
|
|
109
|
+
description?: string;
|
|
110
|
+
/** Columnar params (receive Arrow arrays at process time) */
|
|
111
|
+
params?: P;
|
|
112
|
+
/** Constant params (receive scalar values resolved at bind time) */
|
|
113
|
+
constParams?: Record<string, VgiDataType>;
|
|
114
|
+
/**
|
|
115
|
+
* Per-argument descriptions keyed by param name, surfaced as `vgi_doc` field
|
|
116
|
+
* metadata (and via the extension's `vgi_function_arguments()`). Applies to
|
|
117
|
+
* `params` and `constParams`; for the ordered `parameters` list use each
|
|
118
|
+
* entry's `doc` field instead.
|
|
119
|
+
*/
|
|
120
|
+
argDocs?: Record<string, string>;
|
|
121
|
+
/**
|
|
122
|
+
* Ordered parameter list. When provided, overrides `params` and `constParams`.
|
|
123
|
+
* Use this when parameters need non-default ordering (e.g. const, param, const).
|
|
124
|
+
*/
|
|
125
|
+
parameters?: ScalarParameterDef[];
|
|
126
|
+
/** Output type (static) */
|
|
127
|
+
returns?: R;
|
|
128
|
+
/** Dynamic output type at bind time */
|
|
129
|
+
outputType?: (params: ScalarBindParameters) => VgiDataType | Promise<VgiDataType>;
|
|
130
|
+
/**
|
|
131
|
+
* Value representation for compute I/O. `'rich'` (default) uses JS `Date` for
|
|
132
|
+
* date32/date64 and plain number/bigint elsewhere. `'raw'` uses the branded
|
|
133
|
+
* unit-carrying aliases (Date32, TimestampMicros, UnscaledDecimal, …). The
|
|
134
|
+
* choice flows into compute()'s statically-checked return type and selects
|
|
135
|
+
* the runtime converter used to build the output column.
|
|
136
|
+
*/
|
|
137
|
+
repr?: M;
|
|
138
|
+
/**
|
|
139
|
+
* Process: receives the columnar input batch + const values, returns the
|
|
140
|
+
* output column as an array/iterable of values (statically typed from
|
|
141
|
+
* `returns` and `repr`) or a pre-built VgiBatch.
|
|
142
|
+
*/
|
|
143
|
+
compute: (
|
|
144
|
+
batch: VgiBatch,
|
|
145
|
+
consts: Record<string, any>,
|
|
146
|
+
info: {
|
|
147
|
+
settings: Record<string, any>;
|
|
148
|
+
secrets: Record<string, Record<string, any>>;
|
|
149
|
+
auth: AuthContext;
|
|
150
|
+
}
|
|
151
|
+
// NoInfer pins R/M from `returns`/`repr` so the compute return type is
|
|
152
|
+
// CHECKED against them rather than widening them to fit a wrong value.
|
|
153
|
+
) => ScalarComputeResult<NoInfer<R>, NoInfer<M>>;
|
|
154
|
+
// Metadata
|
|
155
|
+
stability?: FunctionStability;
|
|
156
|
+
nullHandling?: NullHandling;
|
|
157
|
+
examples?: FunctionExample[];
|
|
158
|
+
categories?: string[];
|
|
159
|
+
tags?: Record<string, string>;
|
|
160
|
+
maxWorkers?: number;
|
|
161
|
+
requiredSettings?: string[];
|
|
162
|
+
requiredSecrets?: string[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function defineScalarFunction<
|
|
166
|
+
P extends Record<string, VgiDataType> = Record<string, VgiDataType>,
|
|
167
|
+
R extends VgiDataType = VgiDataType,
|
|
168
|
+
M extends Repr = "rich",
|
|
169
|
+
>(config: ScalarFunctionConfig<P, R, M>): VgiFunction {
|
|
170
|
+
const repr: Repr = config.repr ?? "rich";
|
|
171
|
+
// Build argument specs
|
|
172
|
+
const specs: ArgumentSpec[] = [];
|
|
173
|
+
|
|
174
|
+
if (config.parameters) {
|
|
175
|
+
// Ordered parameter list — supports arbitrary interleaving of const/non-const
|
|
176
|
+
for (const [idx, p] of config.parameters.entries()) {
|
|
177
|
+
const isAny = isNull(p.type);
|
|
178
|
+
specs.push({
|
|
179
|
+
name: p.name,
|
|
180
|
+
position: idx,
|
|
181
|
+
arrowType: isAny ? nullType() : p.type,
|
|
182
|
+
isAnyType: isAny,
|
|
183
|
+
isConst: p.const,
|
|
184
|
+
isVarargs: p.varargs,
|
|
185
|
+
doc: p.doc,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
// Legacy: params first, then constParams
|
|
190
|
+
let posIdx = 0;
|
|
191
|
+
|
|
192
|
+
if (config.params) {
|
|
193
|
+
for (const [name, type] of Object.entries(config.params)) {
|
|
194
|
+
const isAny = isNull(type);
|
|
195
|
+
specs.push({
|
|
196
|
+
name,
|
|
197
|
+
position: posIdx++,
|
|
198
|
+
arrowType: isAny ? nullType() : type,
|
|
199
|
+
isAnyType: isAny,
|
|
200
|
+
doc: config.argDocs?.[name],
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (config.constParams) {
|
|
206
|
+
for (const [name, type] of Object.entries(config.constParams)) {
|
|
207
|
+
specs.push({
|
|
208
|
+
name,
|
|
209
|
+
position: posIdx++,
|
|
210
|
+
arrowType: type,
|
|
211
|
+
isConst: true,
|
|
212
|
+
doc: config.argDocs?.[name],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const meta: FunctionMeta = {
|
|
219
|
+
name: config.name,
|
|
220
|
+
description: config.description,
|
|
221
|
+
stability: config.stability,
|
|
222
|
+
nullHandling: config.nullHandling,
|
|
223
|
+
examples: config.examples,
|
|
224
|
+
categories: config.categories,
|
|
225
|
+
tags: config.tags,
|
|
226
|
+
maxWorkers: config.maxWorkers,
|
|
227
|
+
requiredSettings: config.requiredSettings,
|
|
228
|
+
requiredSecrets: config.requiredSecrets,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Default output schema for catalog registration.
|
|
232
|
+
// Static returns: use the declared type.
|
|
233
|
+
// Dynamic outputType: use null with vgi:any metadata (matches Python convention).
|
|
234
|
+
const defaultOutputSchema = config.returns
|
|
235
|
+
? makeSchema([field("result", config.returns, true)])
|
|
236
|
+
: makeSchema([field("result", nullType(), true, new Map([["vgi:any", "true"]]))]);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
kind: "scalar",
|
|
240
|
+
meta,
|
|
241
|
+
argumentSpecs: specs,
|
|
242
|
+
defaultOutputSchema,
|
|
243
|
+
|
|
244
|
+
async bind(request: BindRequest): Promise<BindResponse> {
|
|
245
|
+
const settings = batchToScalarDict(request.settings);
|
|
246
|
+
const secrets = batchToSecretDict(request.secrets);
|
|
247
|
+
|
|
248
|
+
// Extract constant args from the Arguments.
|
|
249
|
+
// DuckDB only sends const arg values in Arguments (column args are in inputSchema).
|
|
250
|
+
// Arguments are indexed sequentially (0, 1, ...) for const args only.
|
|
251
|
+
const constArgs: Record<string, any> = {};
|
|
252
|
+
{
|
|
253
|
+
let constIdx = 0;
|
|
254
|
+
for (const spec of specs) {
|
|
255
|
+
if (spec.isConst) {
|
|
256
|
+
constArgs[spec.name] = request.arguments.get(constIdx, undefined);
|
|
257
|
+
constIdx++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Determine output type
|
|
263
|
+
let outputType: VgiDataType;
|
|
264
|
+
if (config.outputType) {
|
|
265
|
+
outputType = await config.outputType({
|
|
266
|
+
constArgs,
|
|
267
|
+
argumentsSchema: request.input_schema ?? makeSchema([]),
|
|
268
|
+
settings,
|
|
269
|
+
secrets,
|
|
270
|
+
bindCall: request,
|
|
271
|
+
});
|
|
272
|
+
} else if (config.returns) {
|
|
273
|
+
outputType = config.returns;
|
|
274
|
+
} else {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Scalar function '${config.name}' must specify either 'returns' or 'outputType'`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const outputSchema = makeSchema([
|
|
281
|
+
field("result", outputType, true),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
return { output_schema: outputSchema, opaque_data: null };
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
globalInit(request: InitRequest): GlobalInitResponse {
|
|
288
|
+
const executionId = new Uint8Array(16);
|
|
289
|
+
crypto.getRandomValues(executionId);
|
|
290
|
+
return {
|
|
291
|
+
max_workers: config.maxWorkers ?? DEFAULT_MAX_WORKERS,
|
|
292
|
+
execution_id: executionId,
|
|
293
|
+
opaque_data: null,
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
createStreamHandlers(
|
|
298
|
+
request: InitRequest,
|
|
299
|
+
response: GlobalInitResponse
|
|
300
|
+
): StreamHandlers {
|
|
301
|
+
const outputSchema = request.output_schema;
|
|
302
|
+
const settings = batchToScalarDict(request.bind_call.settings);
|
|
303
|
+
const secrets = batchToSecretDict(request.bind_call.secrets);
|
|
304
|
+
|
|
305
|
+
// Extract const args (DuckDB only sends const values, indexed sequentially)
|
|
306
|
+
const constArgs: Record<string, any> = {};
|
|
307
|
+
let constIdx = 0;
|
|
308
|
+
for (const spec of specs) {
|
|
309
|
+
if (spec.isConst) {
|
|
310
|
+
constArgs[spec.name] = request.bind_call.arguments.get(constIdx, undefined);
|
|
311
|
+
constIdx++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const inputSchema = request.bind_call.input_schema;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
outputSchema,
|
|
319
|
+
inputSchema: inputSchema ?? undefined,
|
|
320
|
+
exchangeInit: () => ({}),
|
|
321
|
+
exchangeFn: (state: any, input: VgiBatch, out: OutputCollector) => {
|
|
322
|
+
const result = config.compute(input as any, constArgs, { settings, secrets, auth: out.auth });
|
|
323
|
+
|
|
324
|
+
// Build output batch
|
|
325
|
+
let outputBatch: VgiBatch;
|
|
326
|
+
if (isBatch(result)) {
|
|
327
|
+
outputBatch = result as VgiBatch;
|
|
328
|
+
} else if (Array.isArray(result)) {
|
|
329
|
+
// Array of values -> single column. Route through batchFromColumns
|
|
330
|
+
// so complex types (Decimal, BigInt-backed Timestamp/Duration,
|
|
331
|
+
// List, Map, Struct) are built via our buildColumnData. The `repr`
|
|
332
|
+
// selects raw<->canonical (branded) vs rich<->canonical conversion.
|
|
333
|
+
const fieldName = outputSchema.fields[0].name;
|
|
334
|
+
outputBatch = batchFromColumns({ [fieldName]: result }, outputSchema, repr);
|
|
335
|
+
} else {
|
|
336
|
+
// User returned an Arrow Vector / typed-array-shaped column. Iterate
|
|
337
|
+
// it into a JS array and re-route through batchFromColumns. Adds an
|
|
338
|
+
// O(n) copy but matches both backends' contract.
|
|
339
|
+
const fieldName = outputSchema.fields[0].name;
|
|
340
|
+
outputBatch = batchFromColumns({ [fieldName]: [...(result as Iterable<any>)] }, outputSchema, repr);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Validate row count
|
|
344
|
+
if (outputBatch.numRows !== input.numRows) {
|
|
345
|
+
throw new RowCountMismatchError(
|
|
346
|
+
input.numRows,
|
|
347
|
+
outputBatch.numRows
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
out.emit(outputBatch);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Cloudflare Durable Object storage for VGI function state.
|
|
3
|
+
//
|
|
4
|
+
// Port of vgi-python's vgi/function_storage_cf_do.py. Talks the Cloudflare
|
|
5
|
+
// Worker + Durable Object's unified state_* / queue_* protocol
|
|
6
|
+
// (vgi-cloudflare-durable-object-storage/src/index.ts). Every request carries
|
|
7
|
+
// the per-attach `shard_key` (set via forShard); destructive ops carry a fresh
|
|
8
|
+
// 32-hex `attempt_id`. The DO is single-threaded SQLite, so ops are atomic.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// Set `VGI_WORKER_SHARED_STORAGE=cloudflare-do` plus `VGI_CF_DO_URL`.
|
|
12
|
+
// Optionally set `VGI_CF_DO_TOKEN` for bearer auth.
|
|
13
|
+
|
|
14
|
+
import type { FunctionStorage } from "./storage.js";
|
|
15
|
+
import { UnknownInvocationError } from "./storage.js";
|
|
16
|
+
|
|
17
|
+
/** Minimal fetch signature satisfied by both the global `fetch` and a
|
|
18
|
+
* Cloudflare service-binding Fetcher (`env.<BINDING>`). */
|
|
19
|
+
export type FetchLike = (
|
|
20
|
+
input: string,
|
|
21
|
+
init?: { method?: string; headers?: Record<string, string>; body?: string },
|
|
22
|
+
) => Promise<Response>;
|
|
23
|
+
|
|
24
|
+
// Namespaces under the unified state_* table for the legacy worker/queue
|
|
25
|
+
// families the DO no longer has dedicated endpoints for.
|
|
26
|
+
const NS_WORKER = new TextEncoder().encode("worker");
|
|
27
|
+
|
|
28
|
+
export class FunctionStorageCfDo implements FunctionStorage {
|
|
29
|
+
private readonly _baseUrl: string;
|
|
30
|
+
private readonly _token: string | null;
|
|
31
|
+
/** Per-attach routing key (att-<hex uuid>); "" until pinned via forShard. */
|
|
32
|
+
private readonly _shardKey: string;
|
|
33
|
+
/** Fetch implementation. Defaults to the global `fetch`; pass a Cloudflare
|
|
34
|
+
* service-binding Fetcher (env.<BINDING>) when the storage worker lives on
|
|
35
|
+
* the same account/zone — a same-zone public-URL fetch is rejected (CF
|
|
36
|
+
* error 1042), but a service binding routes worker-to-worker directly. */
|
|
37
|
+
private readonly _fetch: FetchLike;
|
|
38
|
+
|
|
39
|
+
constructor(opts: {
|
|
40
|
+
url: string;
|
|
41
|
+
token?: string | null;
|
|
42
|
+
shardKey?: string;
|
|
43
|
+
fetch?: FetchLike;
|
|
44
|
+
}) {
|
|
45
|
+
// Strip trailing slash so endpoint paths can be appended unconditionally.
|
|
46
|
+
this._baseUrl = opts.url.replace(/\/+$/, "");
|
|
47
|
+
this._token = opts.token ?? null;
|
|
48
|
+
this._shardKey = opts.shardKey ?? "";
|
|
49
|
+
this._fetch = opts.fetch ?? ((input, init) => fetch(input as any, init));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build an instance from environment variables. */
|
|
53
|
+
static fromEnv(): FunctionStorageCfDo {
|
|
54
|
+
const url = process.env.VGI_CF_DO_URL;
|
|
55
|
+
if (!url) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"VGI_CF_DO_URL environment variable is required when " +
|
|
58
|
+
"VGI_WORKER_SHARED_STORAGE=cloudflare-do",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return new FunctionStorageCfDo({
|
|
62
|
+
url,
|
|
63
|
+
token: process.env.VGI_CF_DO_TOKEN || null,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Return a view of this backend pinned to one shard key, so callers can route
|
|
69
|
+
* per logical ATTACH. Shares the URL/token; only the shard key differs.
|
|
70
|
+
*/
|
|
71
|
+
forShard(shardKey: string): FunctionStorageCfDo {
|
|
72
|
+
return new FunctionStorageCfDo({
|
|
73
|
+
url: this._baseUrl,
|
|
74
|
+
token: this._token,
|
|
75
|
+
shardKey,
|
|
76
|
+
fetch: this._fetch,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Worker state → ns=worker, key = int64(worker_id) ---
|
|
81
|
+
|
|
82
|
+
async workerPut(executionId: Uint8Array, workerId: number, state: Uint8Array): Promise<void> {
|
|
83
|
+
await this._statePutMany(executionId, NS_WORKER, [[int64Key(workerId), state]]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async workerCollect(executionId: Uint8Array): Promise<Uint8Array[]> {
|
|
87
|
+
const rows = await this._statePaged("state_drain", executionId, NS_WORKER, newAttemptId());
|
|
88
|
+
return rows.map(([, v]) => v);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async workerScan(executionId: Uint8Array): Promise<Array<[number, Uint8Array]>> {
|
|
92
|
+
const rows = await this._statePaged("state_scan", executionId, NS_WORKER, null);
|
|
93
|
+
return rows.map(([k, v]) => [int64FromKey(k), v]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Work queue ---
|
|
97
|
+
|
|
98
|
+
async queuePush(executionId: Uint8Array, items: Uint8Array[]): Promise<number> {
|
|
99
|
+
const data = await this._post<{ count: number }>("queue_push", {
|
|
100
|
+
execution_id: bytesToB64(executionId),
|
|
101
|
+
items: items.map(bytesToB64),
|
|
102
|
+
attempt_id: newAttemptId(),
|
|
103
|
+
});
|
|
104
|
+
return Number(data.count);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async queuePop(executionId: Uint8Array): Promise<Uint8Array | null> {
|
|
108
|
+
const data = await this._post<{ item: string | null }>("queue_pop", {
|
|
109
|
+
execution_id: bytesToB64(executionId),
|
|
110
|
+
attempt_id: newAttemptId(),
|
|
111
|
+
});
|
|
112
|
+
return data.item ? b64ToBytes(data.item) : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async queueClear(executionId: Uint8Array): Promise<number> {
|
|
116
|
+
const data = await this._post<{ cleared: number }>("queue_clear", {
|
|
117
|
+
execution_id: bytesToB64(executionId),
|
|
118
|
+
attempt_id: newAttemptId(),
|
|
119
|
+
});
|
|
120
|
+
return Number(data.cleared);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Namespaced key/value state ---
|
|
124
|
+
|
|
125
|
+
async stateGet(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array): Promise<Uint8Array | null> {
|
|
126
|
+
const data = await this._post<{ rows: Array<{ value: string } | null> }>("state_get_many", {
|
|
127
|
+
scope_id: bytesToB64(scopeId),
|
|
128
|
+
ns: bytesToB64(ns),
|
|
129
|
+
keys: [bytesToB64(key)],
|
|
130
|
+
});
|
|
131
|
+
const row = (data.rows ?? [])[0];
|
|
132
|
+
return row ? b64ToBytes(row.value) : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async statePut(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array, value: Uint8Array): Promise<void> {
|
|
136
|
+
await this._statePutMany(scopeId, ns, [[key, value]]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Append-only log ---
|
|
140
|
+
|
|
141
|
+
async stateAppend(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array, item: Uint8Array): Promise<number> {
|
|
142
|
+
const data = await this._post<{ ordinal: number }>("state_append", {
|
|
143
|
+
scope_id: bytesToB64(scopeId),
|
|
144
|
+
ns: bytesToB64(ns),
|
|
145
|
+
key: bytesToB64(key),
|
|
146
|
+
item: bytesToB64(item),
|
|
147
|
+
attempt_id: newAttemptId(),
|
|
148
|
+
});
|
|
149
|
+
return Number(data.ordinal);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async stateLogScan(
|
|
153
|
+
scopeId: Uint8Array,
|
|
154
|
+
ns: Uint8Array,
|
|
155
|
+
key: Uint8Array,
|
|
156
|
+
afterId: number = -1,
|
|
157
|
+
limit: number | null = null,
|
|
158
|
+
): Promise<Array<[number, Uint8Array]>> {
|
|
159
|
+
const body: Record<string, unknown> = {
|
|
160
|
+
scope_id: bytesToB64(scopeId),
|
|
161
|
+
ns: bytesToB64(ns),
|
|
162
|
+
key: bytesToB64(key),
|
|
163
|
+
after_id: afterId,
|
|
164
|
+
};
|
|
165
|
+
if (limit != null && limit > 0) body.limit = limit;
|
|
166
|
+
const data = await this._post<{ rows: Array<{ id: number; value: string }> }>("state_log_scan", body);
|
|
167
|
+
return (data.rows ?? []).map((r) => [Number(r.id), b64ToBytes(r.value)]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async executionClear(scopeId: Uint8Array): Promise<number> {
|
|
171
|
+
const data = await this._post<{ deleted: number }>("execution_clear", {
|
|
172
|
+
scope_id: bytesToB64(scopeId),
|
|
173
|
+
attempt_id: newAttemptId(),
|
|
174
|
+
});
|
|
175
|
+
return Number(data.deleted);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- unified state_* helpers ---
|
|
179
|
+
|
|
180
|
+
private async _statePutMany(
|
|
181
|
+
scopeId: Uint8Array,
|
|
182
|
+
ns: Uint8Array,
|
|
183
|
+
items: Array<[Uint8Array, Uint8Array]>,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
await this._post("state_put_many", {
|
|
186
|
+
scope_id: bytesToB64(scopeId),
|
|
187
|
+
ns: bytesToB64(ns),
|
|
188
|
+
items: items.map(([k, v]) => ({ key: bytesToB64(k), value: bytesToB64(v) })),
|
|
189
|
+
attempt_id: newAttemptId(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Drive state_scan / state_drain across pages. attemptId is null for scan. */
|
|
194
|
+
private async _statePaged(
|
|
195
|
+
endpoint: string,
|
|
196
|
+
scopeId: Uint8Array,
|
|
197
|
+
ns: Uint8Array,
|
|
198
|
+
attemptId: string | null,
|
|
199
|
+
): Promise<Array<[Uint8Array, Uint8Array]>> {
|
|
200
|
+
const out: Array<[Uint8Array, Uint8Array]> = [];
|
|
201
|
+
let afterKey: string | undefined;
|
|
202
|
+
for (;;) {
|
|
203
|
+
const body: Record<string, unknown> = { scope_id: bytesToB64(scopeId), ns: bytesToB64(ns) };
|
|
204
|
+
if (afterKey != null) body.after_key = afterKey;
|
|
205
|
+
if (attemptId != null) body.attempt_id = attemptId;
|
|
206
|
+
const data = await this._post<{ rows: Array<{ key: string; value: string }>; next_after?: string }>(
|
|
207
|
+
endpoint,
|
|
208
|
+
body,
|
|
209
|
+
);
|
|
210
|
+
for (const r of data.rows ?? []) out.push([b64ToBytes(r.key), b64ToBytes(r.value)]);
|
|
211
|
+
if (!data.next_after) break;
|
|
212
|
+
afterKey = data.next_after;
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- HTTP plumbing ---
|
|
218
|
+
|
|
219
|
+
private async _post<T = Record<string, unknown>>(
|
|
220
|
+
endpoint: string,
|
|
221
|
+
body: Record<string, unknown>,
|
|
222
|
+
): Promise<T> {
|
|
223
|
+
const url = `${this._baseUrl}/${endpoint}`;
|
|
224
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
225
|
+
if (this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
226
|
+
// The Worker routes on shard_key (idFromName) and rejects requests without
|
|
227
|
+
// one — always splice it in.
|
|
228
|
+
const payload = JSON.stringify({ ...body, shard_key: this._shardKey });
|
|
229
|
+
|
|
230
|
+
let lastErr: unknown;
|
|
231
|
+
// One retry on transport-level failures, matching the python client.
|
|
232
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
233
|
+
let resp: Response;
|
|
234
|
+
try {
|
|
235
|
+
resp = await this._fetch(url, { method: "POST", headers, body: payload });
|
|
236
|
+
} catch (err) {
|
|
237
|
+
lastErr = err;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let data: any;
|
|
242
|
+
try {
|
|
243
|
+
data = await resp.json();
|
|
244
|
+
} catch {
|
|
245
|
+
if (!resp.ok) {
|
|
246
|
+
throw new Error(`CF DO storage error ${resp.status} on ${endpoint}: <non-json body>`);
|
|
247
|
+
}
|
|
248
|
+
return {} as T;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (resp.status === 404 && data?.error === "unknown_invocation") {
|
|
252
|
+
throw new UnknownInvocationError(
|
|
253
|
+
data.message ??
|
|
254
|
+
"Invocation is not registered. Call queuePush first to register the invocation.",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (resp.status === 401) {
|
|
258
|
+
throw new Error(`Authentication failed: ${data?.error ?? "unauthorized"}`);
|
|
259
|
+
}
|
|
260
|
+
if (!resp.ok) {
|
|
261
|
+
throw new Error(`CF DO storage error ${resp.status} on ${endpoint}: ${JSON.stringify(data)}`);
|
|
262
|
+
}
|
|
263
|
+
return data as T;
|
|
264
|
+
}
|
|
265
|
+
throw lastErr instanceof Error
|
|
266
|
+
? lastErr
|
|
267
|
+
: new Error(`CF DO storage transport error on ${endpoint}: ${String(lastErr)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Fresh 32-char lowercase-hex idempotency token (the DO's attempt_id shape). */
|
|
272
|
+
function newAttemptId(): string {
|
|
273
|
+
const b = new Uint8Array(16);
|
|
274
|
+
crypto.getRandomValues(b);
|
|
275
|
+
let s = "";
|
|
276
|
+
for (let i = 0; i < 16; i++) s += b[i].toString(16).padStart(2, "0");
|
|
277
|
+
return s;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Encode an int64 worker/group id as an 8-byte big-endian state key. */
|
|
281
|
+
function int64Key(v: number): Uint8Array {
|
|
282
|
+
const b = new Uint8Array(8);
|
|
283
|
+
new DataView(b.buffer).setBigInt64(0, BigInt(v), false);
|
|
284
|
+
return b;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function int64FromKey(b: Uint8Array): number {
|
|
288
|
+
if (b.length !== 8) return 0;
|
|
289
|
+
return Number(new DataView(b.buffer, b.byteOffset, 8).getBigInt64(0, false));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function bytesToB64(buf: Uint8Array): string {
|
|
293
|
+
let binary = "";
|
|
294
|
+
for (let i = 0; i < buf.length; i++) binary += String.fromCharCode(buf[i]);
|
|
295
|
+
return btoa(binary);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function b64ToBytes(b64: string): Uint8Array {
|
|
299
|
+
const binary = atob(b64);
|
|
300
|
+
const out = new Uint8Array(binary.length);
|
|
301
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
302
|
+
return out;
|
|
303
|
+
}
|