@deepexi/datasense-connector 3.0.1 → 3.1.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/README.md +158 -139
- package/package.json +19 -28
- package/scripts/build.ts +188 -0
- package/src/define.ts +267 -0
- package/src/descriptor.ts +116 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +108 -0
- package/src/internal/location.ts +23 -0
- package/src/runtime/guest/bridge.ts +238 -0
- package/src/runtime/guest/cache.ts +43 -0
- package/src/runtime/guest/check.ts +17 -0
- package/src/runtime/guest/crypto.ts +166 -0
- package/src/runtime/guest/ctx.ts +157 -0
- package/src/runtime/guest/db.ts +59 -0
- package/src/runtime/guest/failure.ts +69 -0
- package/src/runtime/guest/http.ts +194 -0
- package/src/runtime/guest/index.ts +102 -0
- package/{runtime/guest/limits.d.ts → src/runtime/guest/limits.ts} +12 -12
- package/src/runtime/guest/loader.ts +210 -0
- package/src/runtime/guest/log.ts +47 -0
- package/src/runtime/guest/map.ts +45 -0
- package/src/runtime/guest/operation.ts +193 -0
- package/src/runtime/guest/paginate.ts +169 -0
- package/src/runtime/guest/pull.ts +177 -0
- package/src/runtime/guest/push.ts +92 -0
- package/src/runtime/guest/signal.ts +14 -0
- package/src/runtime/guest/suggest.ts +51 -0
- package/src/runtime/guest/time.ts +137 -0
- package/src/runtime/guest-runtime.ts +75 -0
- package/src/runtime/index.ts +53 -0
- package/src/runtime/protocol.ts +557 -0
- package/src/runtime/session.ts +216 -0
- package/src/testing/child.ts +21 -0
- package/src/testing/compliance.ts +343 -0
- package/src/testing/engine.ts +445 -0
- package/src/testing/fixtures.ts +591 -0
- package/src/testing/gateway.ts +482 -0
- package/src/testing/import-guard.ts +221 -0
- package/src/testing/index.ts +43 -0
- package/src/testing/local-db.ts +226 -0
- package/src/testing/mock-external-system.ts +249 -0
- package/src/testing/pinned-http.ts +144 -0
- package/src/testing/recorder.ts +140 -0
- package/src/testing/redaction.ts +94 -0
- package/src/testing/replay.ts +437 -0
- package/src/testing/run-tests.ts +115 -0
- package/src/testing/schema.ts +70 -0
- package/src/testing/source-loader.ts +374 -0
- package/src/testing/types.ts +78 -0
- package/src/types.ts +329 -0
- package/src/validate.ts +267 -0
- package/test/build.test.ts +159 -0
- package/test/define.test-d.ts +182 -0
- package/test/fixtures/acceptance.ts +156 -0
- package/test/fixtures/connectors/http-pull/.ontos/descriptor.json +72 -0
- package/test/fixtures/connectors/http-pull/.ontos/types.ts +61 -0
- package/test/fixtures/connectors/http-pull/package.json +5 -0
- package/test/fixtures/connectors/http-pull/streams/items/index.ts +37 -0
- package/test/fixtures/connectors/http-pull/streams/items/map.ts +12 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/case.json +15 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/exchanges.ndjson +2 -0
- package/test/fixtures/connectors/pg-pull/.ontos/descriptor.json +151 -0
- package/test/fixtures/connectors/pg-pull/.ontos/types.ts +62 -0
- package/test/fixtures/connectors/pg-pull/lib/sql.ts +14 -0
- package/test/fixtures/connectors/pg-pull/package.json +5 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/index.ts +30 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/case.json +15 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/exchanges.ndjson +3 -0
- package/test/fixtures/connectors/pg-pull/streams/orders/index.ts +32 -0
- package/test/fixtures/descriptor.ts +54 -0
- package/test/fixtures/kingdee-example.ts +82 -0
- package/test/fixtures/record-acceptance.ts +32 -0
- package/test/runtime/ctx.test.ts +434 -0
- package/test/runtime/helpers.ts +157 -0
- package/test/runtime/jobs.test.ts +444 -0
- package/test/runtime/operation.test.ts +175 -0
- package/test/runtime/session.test.ts +229 -0
- package/test/testing/acceptance-fixtures.test.ts +69 -0
- package/test/testing/compliance.test.ts +247 -0
- package/test/testing/engine.test.ts +283 -0
- package/test/testing/fixtures.test.ts +221 -0
- package/test/testing/gateway.test.ts +337 -0
- package/test/testing/import-guard.test.ts +45 -0
- package/test/testing/mock-external-system.test.ts +144 -0
- package/test/testing/operation-fixtures.test.ts +160 -0
- package/test/testing/recorder.test.ts +163 -0
- package/test/testing/redaction.test.ts +54 -0
- package/test/testing/replay.test.ts +207 -0
- package/test/testing/run-tests.test.ts +137 -0
- package/test/testing/source-loader.test.ts +220 -0
- package/test/validate-config.test.ts +104 -0
- package/tsconfig.build.json +23 -0
- package/tsconfig.json +19 -0
- package/cjs/index.cjs +0 -1015
- package/cjs/runtime.cjs +0 -752
- package/cjs/testing.cjs +0 -3591
- package/errors.d.ts +0 -17
- package/errors.js +0 -53
- package/guest.cjs +0 -2659
- package/index.d.ts +0 -13
- package/index.js +0 -7
- package/internal/location.d.ts +0 -6
- package/internal/location.js +0 -8
- package/presentation.d.ts +0 -38
- package/presentation.js +0 -268
- package/runtime/guest/bridge.d.ts +0 -53
- package/runtime/guest/bridge.js +0 -187
- package/runtime/guest/cache.d.ts +0 -3
- package/runtime/guest/cache.js +0 -42
- package/runtime/guest/crypto.d.ts +0 -13
- package/runtime/guest/crypto.js +0 -177
- package/runtime/guest/ctx.d.ts +0 -30
- package/runtime/guest/ctx.js +0 -124
- package/runtime/guest/db.d.ts +0 -6
- package/runtime/guest/db.js +0 -59
- package/runtime/guest/discover.d.ts +0 -29
- package/runtime/guest/discover.js +0 -51
- package/runtime/guest/failure.d.ts +0 -24
- package/runtime/guest/failure.js +0 -63
- package/runtime/guest/http.d.ts +0 -4
- package/runtime/guest/http.js +0 -204
- package/runtime/guest/index.d.ts +0 -12
- package/runtime/guest/index.js +0 -89
- package/runtime/guest/limits.js +0 -12
- package/runtime/guest/loader.d.ts +0 -15
- package/runtime/guest/loader.js +0 -42
- package/runtime/guest/log.d.ts +0 -10
- package/runtime/guest/log.js +0 -36
- package/runtime/guest/map.d.ts +0 -4
- package/runtime/guest/map.js +0 -45
- package/runtime/guest/paginate.d.ts +0 -6
- package/runtime/guest/paginate.js +0 -180
- package/runtime/guest/pull.d.ts +0 -12
- package/runtime/guest/pull.js +0 -150
- package/runtime/guest/push.d.ts +0 -18
- package/runtime/guest/push.js +0 -80
- package/runtime/guest/signal.d.ts +0 -3
- package/runtime/guest/signal.js +0 -12
- package/runtime/guest/time.d.ts +0 -5
- package/runtime/guest/time.js +0 -127
- package/runtime/guest-runtime.d.ts +0 -7
- package/runtime/guest-runtime.js +0 -51
- package/runtime/index.d.ts +0 -18
- package/runtime/index.js +0 -3
- package/runtime/protocol.d.ts +0 -276
- package/runtime/protocol.js +0 -409
- package/runtime/session.d.ts +0 -12
- package/runtime/session.js +0 -191
- package/t.d.ts +0 -18
- package/t.js +0 -26
- package/testing/child.cjs +0 -350
- package/testing/child.d.ts +0 -1
- package/testing/child.js +0 -12
- package/testing/compliance.d.ts +0 -41
- package/testing/compliance.js +0 -291
- package/testing/engine.d.ts +0 -66
- package/testing/engine.js +0 -339
- package/testing/fixtures.d.ts +0 -145
- package/testing/fixtures.js +0 -407
- package/testing/gateway.d.ts +0 -65
- package/testing/gateway.js +0 -402
- package/testing/import-guard.d.ts +0 -57
- package/testing/import-guard.js +0 -187
- package/testing/index.d.ts +0 -33
- package/testing/index.js +0 -10
- package/testing/local-db.d.ts +0 -27
- package/testing/local-db.js +0 -181
- package/testing/pinned-http.d.ts +0 -14
- package/testing/pinned-http.js +0 -125
- package/testing/recorder.d.ts +0 -15
- package/testing/recorder.js +0 -116
- package/testing/redaction.d.ts +0 -28
- package/testing/redaction.js +0 -67
- package/testing/replay.d.ts +0 -50
- package/testing/replay.js +0 -313
- package/testing/schema.d.ts +0 -13
- package/testing/schema.js +0 -65
- package/testing/source-loader.d.ts +0 -19
- package/testing/source-loader.js +0 -238
- package/testing/types.d.ts +0 -79
- package/testing/types.js +0 -18
- package/types.d.ts +0 -302
- package/types.js +0 -13
- package/validate.d.ts +0 -78
- package/validate.js +0 -597
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 推送作业:对每条请求按推送名找处理器,先 `verify` 再 `map`。行为与第 1 期一致:
|
|
3
|
+
* - `ctx.record` / `ctx.remove` 直接记录,请求成功后才并入结果;请求结束后再用该 `ctx` 抛错;
|
|
4
|
+
* - 单条请求抛错只拒收该条(code 取错误的 `code`,普通异常为 `CONNECTOR_ERROR`),已产生的记录丢弃;
|
|
5
|
+
* - `SCHEMA_ERROR` / `FATAL_ERROR` 让整次执行失败;
|
|
6
|
+
* - 推送名在描述里不存在的请求拒收,code `PUSH_UNKNOWN`。
|
|
7
|
+
*
|
|
8
|
+
* 记录里不属于该推送的表或未声明的字段照常交给宿主,由宿主(connector)丢弃并告警。
|
|
9
|
+
*/
|
|
10
|
+
import type { PushContext, PushRequest } from '../../types';
|
|
11
|
+
import type { GuestJob, GuestPushResult } from '../protocol';
|
|
12
|
+
import { jsonParse, jsonStringify } from './bridge';
|
|
13
|
+
import { deepFreeze } from './ctx';
|
|
14
|
+
import { connectorStack, errorInfo, failure, type FailureMessage } from './failure';
|
|
15
|
+
import type { LoadedConnector } from './loader';
|
|
16
|
+
import { createLogBuffer } from './log';
|
|
17
|
+
import { isoNow } from './time';
|
|
18
|
+
|
|
19
|
+
type PushJob = Extract<GuestJob, { kind: 'push' }>;
|
|
20
|
+
|
|
21
|
+
const WHOLE_RUN_FAILURES: readonly string[] = ['SCHEMA_ERROR', 'FATAL_ERROR'];
|
|
22
|
+
|
|
23
|
+
/** 把连接器给的行复制成纯 JSON 数据;不可序列化时抛错,按该请求的连接器异常处理。 */
|
|
24
|
+
function copyRecord(value: unknown, what: string): Record<string, unknown> {
|
|
25
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${what} must be an object`);
|
|
26
|
+
const text = jsonStringify(value);
|
|
27
|
+
if (typeof text !== 'string') throw new TypeError(`${what} is not serializable`);
|
|
28
|
+
return jsonParse(text) as Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runPush(connector: LoadedConnector, job: PushJob): Promise<{ type: 'result'; result: GuestPushResult } | FailureMessage> {
|
|
32
|
+
const handlers = connector.pushes;
|
|
33
|
+
if (Object.keys(handlers).length === 0) return failure('CONNECTOR_LOAD_FAILED', 'connector does not support push', []);
|
|
34
|
+
const config = deepFreeze(job.config);
|
|
35
|
+
const logs = createLogBuffer();
|
|
36
|
+
const result: GuestPushResult = { kind: 'push', records: [], removals: [], rejected: [], logs: [] };
|
|
37
|
+
|
|
38
|
+
for (const request of job.requests) {
|
|
39
|
+
const requestId = request.requestId;
|
|
40
|
+
const handler = Object.hasOwn(handlers, request.push) ? handlers[request.push] : undefined;
|
|
41
|
+
if (!handler) {
|
|
42
|
+
result.rejected.push({ requestId, code: 'PUSH_UNKNOWN', message: `connector has no push named ${request.push}` });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const pending: { records: GuestPushResult['records']; removals: GuestPushResult['removals']; open: boolean } = { records: [], removals: [], open: true };
|
|
46
|
+
const assertOpen = () => {
|
|
47
|
+
if (!pending.open) throw new TypeError('ctx is no longer usable after the request finished');
|
|
48
|
+
};
|
|
49
|
+
const ctx: PushContext = Object.freeze({
|
|
50
|
+
config,
|
|
51
|
+
record(table: string, row: Record<string, unknown>) {
|
|
52
|
+
assertOpen();
|
|
53
|
+
if (typeof table !== 'string') throw new TypeError('table must be a string');
|
|
54
|
+
pending.records.push({ requestId, table, row: copyRecord(row, 'row') });
|
|
55
|
+
},
|
|
56
|
+
remove(table: string, key: Record<string, unknown>) {
|
|
57
|
+
assertOpen();
|
|
58
|
+
if (typeof table !== 'string') throw new TypeError('table must be a string');
|
|
59
|
+
pending.removals.push({ requestId, table, key: copyRecord(key, 'key') });
|
|
60
|
+
},
|
|
61
|
+
log: logs.logger(requestId, () => pending.open),
|
|
62
|
+
time: Object.freeze({ now: isoNow }),
|
|
63
|
+
});
|
|
64
|
+
const body = request.body;
|
|
65
|
+
const req: PushRequest = Object.freeze({
|
|
66
|
+
id: requestId,
|
|
67
|
+
receivedAt: request.receivedAt,
|
|
68
|
+
headers: deepFreeze({ ...request.headers }),
|
|
69
|
+
body,
|
|
70
|
+
json<T = unknown>(): T {
|
|
71
|
+
return jsonParse(body) as T;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
if (typeof handler.verify === 'function') await Reflect.apply(handler.verify, handler, [ctx, req]);
|
|
77
|
+
await Reflect.apply(handler.map, handler, [ctx, req]);
|
|
78
|
+
pending.open = false;
|
|
79
|
+
result.records.push(...pending.records);
|
|
80
|
+
result.removals.push(...pending.removals);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
pending.open = false;
|
|
83
|
+
const info = errorInfo(error);
|
|
84
|
+
if (info.code !== null && WHOLE_RUN_FAILURES.includes(info.code)) {
|
|
85
|
+
return failure(info.code as 'SCHEMA_ERROR' | 'FATAL_ERROR', info.message, logs.drain(), connectorStack(info.stack));
|
|
86
|
+
}
|
|
87
|
+
result.rejected.push({ requestId, code: info.code ?? 'CONNECTOR_ERROR', message: info.message });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
result.logs = logs.drain();
|
|
91
|
+
return { type: 'result', result };
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** `ctx.signal`:父进程发 `cancel` 时置位并调用 `onAbort` 回调。 */
|
|
2
|
+
import type { ConnectorSignal } from '../../types';
|
|
3
|
+
import { isAborted, onAbort } from './bridge';
|
|
4
|
+
|
|
5
|
+
export function createSignal(): ConnectorSignal {
|
|
6
|
+
const signal = {
|
|
7
|
+
onAbort(fn: () => void): void {
|
|
8
|
+
if (typeof fn !== 'function') throw new TypeError('ctx.signal.onAbort expects a function');
|
|
9
|
+
onAbort(fn);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(signal, 'aborted', { get: isAborted, enumerable: true });
|
|
13
|
+
return Object.freeze(signal) as ConnectorSignal;
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 建议列作业:调数据流的 `suggestColumns(ctx, { table })`,从外部系统读出可扩展表的可用字段。
|
|
3
|
+
* - 数据流没有实现建议列、或 `table` 不是该数据流的可扩展表:`CONNECTOR_LOAD_FAILED`;
|
|
4
|
+
* - 返回值复制成纯 JSON 后经 `validateSuggestedColumns` 检查,不合法时 `SUGGEST_INVALID`;
|
|
5
|
+
* - 与该表基础列同名的建议去掉(记 warn 日志),其余原样作为结果。
|
|
6
|
+
*/
|
|
7
|
+
import { validateSuggestedColumns } from '../../validate';
|
|
8
|
+
import type { GuestJob, GuestResult } from '../protocol';
|
|
9
|
+
import { jsonParse, jsonStringify, throwIfAborted } from './bridge';
|
|
10
|
+
import { createCheckContext, type ContextOptions } from './ctx';
|
|
11
|
+
import { failure, failureFromError, type FailureMessage } from './failure';
|
|
12
|
+
import type { LoadedConnector } from './loader';
|
|
13
|
+
|
|
14
|
+
type SuggestJob = Extract<GuestJob, { kind: 'suggest' }>;
|
|
15
|
+
|
|
16
|
+
function plain(value: unknown): unknown {
|
|
17
|
+
let text: unknown;
|
|
18
|
+
try {
|
|
19
|
+
text = jsonStringify(value);
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return typeof text === 'string' ? jsonParse(text) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runSuggest(connector: LoadedConnector, job: SuggestJob, options: ContextOptions): Promise<{ type: 'result'; result: GuestResult } | FailureMessage> {
|
|
27
|
+
const suggest = Object.hasOwn(connector.suggestColumns, job.stream) ? connector.suggestColumns[job.stream] : undefined;
|
|
28
|
+
if (!suggest) return failure('CONNECTOR_LOAD_FAILED', `stream ${job.stream} does not implement suggestColumns`, options.logs.drain());
|
|
29
|
+
const stream = connector.descriptor.streams[job.stream];
|
|
30
|
+
const table = stream && Object.hasOwn(stream.tables, job.table) ? stream.tables[job.table] : undefined;
|
|
31
|
+
if (!table || table.extensible !== true) {
|
|
32
|
+
return failure('CONNECTOR_LOAD_FAILED', `table ${job.table} is not an extensible table of stream ${job.stream}`, options.logs.drain());
|
|
33
|
+
}
|
|
34
|
+
const ctx = createCheckContext(options);
|
|
35
|
+
let value: unknown;
|
|
36
|
+
try {
|
|
37
|
+
value = plain(await Reflect.apply(suggest, undefined, [ctx, Object.freeze({ table: job.table })]));
|
|
38
|
+
throwIfAborted();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return failureFromError(error, options.logs.drain());
|
|
41
|
+
}
|
|
42
|
+
const checked = validateSuggestedColumns(value);
|
|
43
|
+
if (!checked.ok) {
|
|
44
|
+
const issues = checked.issues.map((issue) => `${issue.path || '(root)'}: ${issue.message}`).join('; ');
|
|
45
|
+
return failure('SUGGEST_INVALID', `suggestColumns returned invalid columns: ${issues}`, options.logs.drain());
|
|
46
|
+
}
|
|
47
|
+
const base = checked.columns.filter((column) => Object.hasOwn(table.columns, column.name)).map((column) => column.name);
|
|
48
|
+
if (base.length > 0) options.logs.logger().warn(`suggestColumns returned base column(s) of table ${job.table}, ignored: ${base.join(', ')}`);
|
|
49
|
+
const columns = checked.columns.filter((column) => !Object.hasOwn(table.columns, column.name));
|
|
50
|
+
return { type: 'result', result: { kind: 'suggest', columns, logs: options.logs.drain() } };
|
|
51
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** `ctx.time`:`now()` 与 `toUtc(本地时间文本, 时区, 格式?)`。时区换算用上下文自己的 `Intl.DateTimeFormat`。 */
|
|
2
|
+
import type { ConnectorTime } from '../../types';
|
|
3
|
+
|
|
4
|
+
const DateCtor = Date;
|
|
5
|
+
const dateUtc = Date.UTC;
|
|
6
|
+
const dateParse = Date.parse;
|
|
7
|
+
const reflectApply = Reflect.apply;
|
|
8
|
+
const toISOString = Date.prototype.toISOString;
|
|
9
|
+
const DateTimeFormat = Intl.DateTimeFormat;
|
|
10
|
+
|
|
11
|
+
export function isoNow(): string {
|
|
12
|
+
return reflectApply(toISOString, new DateCtor(), []) as string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function iso(ms: number): string {
|
|
16
|
+
return reflectApply(toISOString, new DateCtor(ms), []) as string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PATTERN = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?)?$/;
|
|
20
|
+
const ZONED_PATTERN = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
21
|
+
|
|
22
|
+
interface WallClock { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number }
|
|
23
|
+
|
|
24
|
+
type Field = keyof WallClock;
|
|
25
|
+
const TOKENS: ReadonlyArray<{ token: string; field: Field; pattern: string }> = [
|
|
26
|
+
{ token: 'YYYY', field: 'year', pattern: '(\\d{4})' },
|
|
27
|
+
{ token: 'SSS', field: 'millisecond', pattern: '(\\d{1,3})' },
|
|
28
|
+
{ token: 'MM', field: 'month', pattern: '(\\d{2})' },
|
|
29
|
+
{ token: 'DD', field: 'day', pattern: '(\\d{2})' },
|
|
30
|
+
{ token: 'HH', field: 'hour', pattern: '(\\d{2})' },
|
|
31
|
+
{ token: 'mm', field: 'minute', pattern: '(\\d{2})' },
|
|
32
|
+
{ token: 'ss', field: 'second', pattern: '(\\d{2})' },
|
|
33
|
+
{ token: 'M', field: 'month', pattern: '(\\d{1,2})' },
|
|
34
|
+
{ token: 'D', field: 'day', pattern: '(\\d{1,2})' },
|
|
35
|
+
{ token: 'H', field: 'hour', pattern: '(\\d{1,2})' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function escapeRegExp(text: string): string {
|
|
39
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseWithFormat(local: string, format: string): WallClock | null {
|
|
43
|
+
const fields: Field[] = [];
|
|
44
|
+
let source = '';
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < format.length) {
|
|
47
|
+
const match = TOKENS.find((entry) => format.startsWith(entry.token, i));
|
|
48
|
+
if (match) {
|
|
49
|
+
fields.push(match.field);
|
|
50
|
+
source += match.pattern;
|
|
51
|
+
i += match.token.length;
|
|
52
|
+
} else {
|
|
53
|
+
source += escapeRegExp(format.charAt(i));
|
|
54
|
+
i += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const result = new RegExp(`^${source}$`).exec(local);
|
|
58
|
+
if (!result || !fields.includes('year') || !fields.includes('month') || !fields.includes('day')) return null;
|
|
59
|
+
const clock: WallClock = { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 0, millisecond: 0 };
|
|
60
|
+
fields.forEach((field, index) => {
|
|
61
|
+
const text = result[index + 1] ?? '0';
|
|
62
|
+
clock[field] = field === 'millisecond' ? Number(text.padEnd(3, '0')) : Number(text);
|
|
63
|
+
});
|
|
64
|
+
return clock;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseDefault(local: string): WallClock | null {
|
|
68
|
+
const match = DEFAULT_PATTERN.exec(local);
|
|
69
|
+
if (!match) return null;
|
|
70
|
+
return {
|
|
71
|
+
year: Number(match[1]), month: Number(match[2]), day: Number(match[3]),
|
|
72
|
+
hour: Number(match[4] ?? 0), minute: Number(match[5] ?? 0), second: Number(match[6] ?? 0),
|
|
73
|
+
millisecond: Number((match[7] ?? '0').slice(0, 3).padEnd(3, '0')),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 墙上时间按 UTC 解释的毫秒数;字段越界(如 2 月 30 日)返回 null。 */
|
|
78
|
+
function wallMillis(clock: WallClock): number | null {
|
|
79
|
+
const ms = dateUtc(clock.year, clock.month - 1, clock.day, clock.hour, clock.minute, clock.second, clock.millisecond);
|
|
80
|
+
if (Number.isNaN(ms)) return null;
|
|
81
|
+
const check = new DateCtor(ms);
|
|
82
|
+
if (check.getUTCFullYear() !== clock.year || check.getUTCMonth() !== clock.month - 1 || check.getUTCDate() !== clock.day
|
|
83
|
+
|| check.getUTCHours() !== clock.hour || check.getUTCMinutes() !== clock.minute || check.getUTCSeconds() !== clock.second) return null;
|
|
84
|
+
return ms;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const formatters = new Map<string, Intl.DateTimeFormat>();
|
|
88
|
+
|
|
89
|
+
function formatter(timeZone: string): Intl.DateTimeFormat {
|
|
90
|
+
let cached = formatters.get(timeZone);
|
|
91
|
+
if (!cached) {
|
|
92
|
+
try {
|
|
93
|
+
cached = new DateTimeFormat('en-US', {
|
|
94
|
+
timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric',
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
throw new TypeError(`unknown time zone ${timeZone}`);
|
|
98
|
+
}
|
|
99
|
+
formatters.set(timeZone, cached);
|
|
100
|
+
}
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 时区在 `utcMs` 时刻相对 UTC 的偏移(毫秒)。 */
|
|
105
|
+
function zoneOffset(timeZone: string, utcMs: number): number {
|
|
106
|
+
const base = utcMs - (((utcMs % 1000) + 1000) % 1000);
|
|
107
|
+
const parts: Record<string, number> = {};
|
|
108
|
+
for (const part of formatter(timeZone).formatToParts(new DateCtor(base))) {
|
|
109
|
+
if (part.type !== 'literal') parts[part.type] = Number(part.value);
|
|
110
|
+
}
|
|
111
|
+
const asUtc = dateUtc(parts.year ?? 0, (parts.month ?? 1) - 1, parts.day ?? 1, parts.hour ?? 0, parts.minute ?? 0, parts.second ?? 0);
|
|
112
|
+
return asUtc - base;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function toUtc(local: string, timeZone: string, format?: string): string {
|
|
116
|
+
if (typeof local !== 'string') throw new TypeError('ctx.time.toUtc expects a date-time string');
|
|
117
|
+
if (typeof timeZone !== 'string' || timeZone.length === 0) throw new TypeError('ctx.time.toUtc expects a time zone like Asia/Shanghai');
|
|
118
|
+
const text = local.trim();
|
|
119
|
+
if (format === undefined && ZONED_PATTERN.test(text)) {
|
|
120
|
+
// 文本自带时区偏移:直接按它解析,忽略 timeZone。
|
|
121
|
+
const ms = dateParse(text.replace(' ', 'T'));
|
|
122
|
+
if (Number.isNaN(ms)) throw new TypeError(`invalid date-time ${local}`);
|
|
123
|
+
return iso(ms);
|
|
124
|
+
}
|
|
125
|
+
const clock = format === undefined ? parseDefault(text) : parseWithFormat(text, format);
|
|
126
|
+
const wall = clock ? wallMillis(clock) : null;
|
|
127
|
+
if (wall === null) throw new TypeError(`invalid date-time ${local}${format === undefined ? '' : ` for format ${format}`}`);
|
|
128
|
+
const first = zoneOffset(timeZone, wall);
|
|
129
|
+
let utc = wall - first;
|
|
130
|
+
const second = zoneOffset(timeZone, utc);
|
|
131
|
+
if (second !== first) utc = wall - second;
|
|
132
|
+
return iso(utc);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createTime(): ConnectorTime {
|
|
136
|
+
return Object.freeze({ now: isoNow, toUtc });
|
|
137
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guest 运行时的位置与源码串(宿主侧)。
|
|
3
|
+
*
|
|
4
|
+
* - `GUEST_ENTRY_PATH`:源码下指向 `src/runtime/guest/index.ts`(runner 用 `bundleTrustedEntry` 自己打包);
|
|
5
|
+
* dist 下指向编译后的 `runtime/guest/index.js`,它与预打包的 `guest.cjs` 出自同一份源码。
|
|
6
|
+
* - `loadGuestRuntime()`:dist 下读 `guest.cjs`(构建时打好,Node 可用);源码下用 `Bun.build` 打包,参数与 shared 的
|
|
7
|
+
* `bundleTrustedEntry` 一致(cjs、browser、SDK 内联、无外部依赖、`macros: false`)。结果进程内缓存,失败不缓存。
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { IS_SOURCE, moduleFile, PACKAGE_ROOT } from '../internal/location';
|
|
12
|
+
|
|
13
|
+
export const GUEST_ENTRY_PATH: string = moduleFile('runtime/guest/index');
|
|
14
|
+
|
|
15
|
+
/** dist 根目录下预打包的 guest 运行时文件名。 */
|
|
16
|
+
export const GUEST_BUNDLE_FILE = 'guest.cjs';
|
|
17
|
+
|
|
18
|
+
type BunBuild = typeof Bun.build;
|
|
19
|
+
type BunBuildConfig = Parameters<BunBuild>[0];
|
|
20
|
+
|
|
21
|
+
const CODE_FILE = /\.[cm]?[jt]sx?$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bun 1.3 起:当前进程已 import 过的源文件再交给 `Bun.build` 会报「Unexpected reading file」,经插件自己读文件绕开
|
|
25
|
+
* (与 shared `connector-bundle.ts` 的 `readSourcePlugin` 相同)。
|
|
26
|
+
*/
|
|
27
|
+
function readSourcePlugin(bun: typeof Bun): NonNullable<BunBuildConfig['plugins']>[number] {
|
|
28
|
+
return {
|
|
29
|
+
name: 'datasense-connector-read-source',
|
|
30
|
+
setup(build) {
|
|
31
|
+
build.onLoad({ filter: CODE_FILE }, async (args) => {
|
|
32
|
+
const extension = args.path.slice(args.path.lastIndexOf('.') + 1);
|
|
33
|
+
const loader = extension === 'tsx' ? 'tsx' : extension === 'jsx' ? 'jsx' : /^[cm]?js$/.test(extension) ? 'js' : 'ts';
|
|
34
|
+
return { contents: await bun.file(args.path).text(), loader };
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `Bun.build` 的 `macros: false`(bun-types 未声明);与 shared 的 `MACROS_OFF` 相同。 */
|
|
41
|
+
const MACROS_OFF = { macros: false } as unknown as Partial<BunBuildConfig>;
|
|
42
|
+
|
|
43
|
+
/** 用 Bun 把 guest 入口打成 CJS 字符串(SDK 内联)。构建脚本与源码下的 `loadGuestRuntime` 共用。 */
|
|
44
|
+
export async function bundleGuestWithBun(entry: string = GUEST_ENTRY_PATH): Promise<string> {
|
|
45
|
+
const bun = (globalThis as { Bun?: typeof Bun }).Bun;
|
|
46
|
+
if (!bun) throw new Error('the guest runtime can only be bundled from source under Bun; use the built package (dist) under Node.js');
|
|
47
|
+
const result = await bun.build({
|
|
48
|
+
entrypoints: [entry],
|
|
49
|
+
format: 'cjs',
|
|
50
|
+
target: 'browser',
|
|
51
|
+
minify: false,
|
|
52
|
+
external: [],
|
|
53
|
+
plugins: [readSourcePlugin(bun)],
|
|
54
|
+
throw: false,
|
|
55
|
+
...MACROS_OFF,
|
|
56
|
+
} as BunBuildConfig);
|
|
57
|
+
const output = result.outputs.find((item) => item.kind === 'entry-point');
|
|
58
|
+
if (!result.success || !output) {
|
|
59
|
+
throw new Error(`guest runtime bundle failed: ${result.logs.map((log) => log.message).join('; ') || 'no output'}`);
|
|
60
|
+
}
|
|
61
|
+
return output.text();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let cached: Promise<string> | null = null;
|
|
65
|
+
|
|
66
|
+
/** 可放进 `execute.guest` 的 guest 运行时 CJS 字符串(进程内缓存)。 */
|
|
67
|
+
export function loadGuestRuntime(): Promise<string> {
|
|
68
|
+
if (cached) return cached;
|
|
69
|
+
const pending = IS_SOURCE ? bundleGuestWithBun() : readFile(path.join(PACKAGE_ROOT, GUEST_BUNDLE_FILE), 'utf8');
|
|
70
|
+
cached = pending;
|
|
71
|
+
pending.catch(() => {
|
|
72
|
+
if (cached === pending) cached = null;
|
|
73
|
+
});
|
|
74
|
+
return pending;
|
|
75
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@deepexi/datasense-connector/runtime`:宿主侧入口(runner 的执行子进程与父进程、`./testing` 用),不在 vm 里加载。
|
|
3
|
+
*
|
|
4
|
+
* - `GUEST_ENTRY_PATH`:guest 运行时入口(源码下是 `src/runtime/guest/index.ts`,dist 下是编译后的 `runtime/guest/index.js`),
|
|
5
|
+
* runner 用 `bundleTrustedEntry` 打成 CJS 字符串,放进 `execute.guest`;
|
|
6
|
+
* - `loadGuestRuntime`:直接拿到可放进 `execute.guest` 的 CJS 字符串:dist 下读预打包的 `guest.cjs`,源码下用 `Bun.build` 打包(进程内缓存);
|
|
7
|
+
* - `createGuestSession`:子进程里建 vm 上下文、加载 guest 与连接器代码,之后只转发字符串消息;
|
|
8
|
+
* - `parseParentMessage` / `parseChildMessage`:两侧解析对方消息,结构不合法返回 null;`undeclaredEmitTable` 区分
|
|
9
|
+
* 「`emit` 写了没声明的表」(宿主按 `SCHEMA_ERROR` 结束)与其他不合法消息。
|
|
10
|
+
*
|
|
11
|
+
* 连接器打包产物导出 `{ descriptor, implementations }`(虚拟入口契约)。guest 加载后先核对 `expect` 的名字与版本、
|
|
12
|
+
* 实现与描述是否一一对应,再执行作业;宿主需要的信息(出站主机、生效限流、该数据流的表)由宿主从描述派生,guest 不上报。
|
|
13
|
+
*
|
|
14
|
+
* 连接器操作作业(`kind: 'operation'`)的结果总是 `GuestOperationResult`(失败也走 `result`),只有加载失败走 `failure`;
|
|
15
|
+
* `simulate` 阶段不允许任何宿主调用,其余阶段不允许 `db.*` / `emit`。`suggest` 作业不允许 `emit`。
|
|
16
|
+
* `emit` 的 payload 是 `GuestPullBatch`(不带 `seq`,由宿主编号),回复 `EmitReply`(`{ stop: true }` 时提前结束)。
|
|
17
|
+
*/
|
|
18
|
+
export { GUEST_ENTRY_PATH, loadGuestRuntime } from './guest-runtime';
|
|
19
|
+
export { createGuestSession } from './session';
|
|
20
|
+
export { GUEST_FAILURE_CODES, GUEST_OPERATION_FAILURE_CODES, HOST_METHODS, parseChildMessage, parseParentMessage, undeclaredEmitTable } from './protocol';
|
|
21
|
+
export type {
|
|
22
|
+
CacheEntry,
|
|
23
|
+
CacheGetRequest,
|
|
24
|
+
CacheSetRequest,
|
|
25
|
+
ChildMessage,
|
|
26
|
+
CryptoRequest,
|
|
27
|
+
CryptoResponse,
|
|
28
|
+
DbCursorRequest,
|
|
29
|
+
DbNextResponse,
|
|
30
|
+
DbQueryRequest,
|
|
31
|
+
DbQueryResponse,
|
|
32
|
+
EgressHttpRequest,
|
|
33
|
+
EgressHttpResponse,
|
|
34
|
+
EmitReply,
|
|
35
|
+
GuestFailureCode,
|
|
36
|
+
GuestJob,
|
|
37
|
+
GuestLog,
|
|
38
|
+
GuestOperationFailureCode,
|
|
39
|
+
GuestOperationJob,
|
|
40
|
+
GuestOperationPhase,
|
|
41
|
+
GuestOperationResult,
|
|
42
|
+
GuestPullBatch,
|
|
43
|
+
GuestPushRequest,
|
|
44
|
+
GuestPushResult,
|
|
45
|
+
GuestResult,
|
|
46
|
+
HostCallError,
|
|
47
|
+
HostMethod,
|
|
48
|
+
JobRateLimit,
|
|
49
|
+
LookupAnswer,
|
|
50
|
+
ParentMessage,
|
|
51
|
+
SecretRevealRequest,
|
|
52
|
+
SecretRevealResponse,
|
|
53
|
+
} from './protocol';
|