@tier0/sdk-v2 0.9.9 → 0.10.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/CHANGELOG.md +102 -0
- package/LICENSE +21 -21
- package/README.md +165 -27
- package/dist/cjs/client.d.ts +79 -3
- package/dist/cjs/client.js +4 -6
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/cloud-server.d.ts +33 -26
- package/dist/cjs/cloud-server.js +102 -100
- package/dist/cjs/cloud-server.js.map +1 -1
- package/dist/cjs/cloud-transport.d.ts +15 -20
- package/dist/cjs/cloud-transport.js +87 -68
- package/dist/cjs/cloud-transport.js.map +1 -1
- package/dist/cjs/connection-diagnostics.d.ts +2 -0
- package/dist/cjs/connection-diagnostics.js +20 -0
- package/dist/cjs/connection-diagnostics.js.map +1 -0
- package/dist/cjs/data/bootstrap.js +3 -4
- package/dist/cjs/data/bootstrap.js.map +1 -1
- package/dist/cjs/data/call.d.ts +1 -3
- package/dist/cjs/data/call.js +8 -12
- package/dist/cjs/data/call.js.map +1 -1
- package/dist/cjs/data/client.d.ts +8 -7
- package/dist/cjs/data/client.js +24 -11
- package/dist/cjs/data/client.js.map +1 -1
- package/dist/cjs/data/collection.d.ts +3 -5
- package/dist/cjs/data/collection.js +3 -4
- package/dist/cjs/data/collection.js.map +1 -1
- package/dist/cjs/data/context.d.ts +5 -11
- package/dist/cjs/data/context.js +21 -36
- package/dist/cjs/data/context.js.map +1 -1
- package/dist/cjs/data/error.d.ts +1 -1
- package/dist/cjs/data/error.js +3 -3
- package/dist/cjs/data/error.js.map +1 -1
- package/dist/cjs/data/index.d.ts +5 -3
- package/dist/cjs/data/index.js +5 -5
- package/dist/cjs/data/index.js.map +1 -1
- package/dist/cjs/data/relay.d.ts +51 -0
- package/dist/cjs/data/relay.js +371 -0
- package/dist/cjs/data/relay.js.map +1 -0
- package/dist/cjs/data/subscribe.d.ts +22 -38
- package/dist/cjs/data/subscribe.js +22 -28
- package/dist/cjs/data/subscribe.js.map +1 -1
- package/dist/cjs/event-stream.d.ts +28 -0
- package/dist/cjs/event-stream.js +222 -0
- package/dist/cjs/event-stream.js.map +1 -0
- package/dist/cjs/mq/client.d.ts +32 -1
- package/dist/cjs/mq/client.js +404 -69
- package/dist/cjs/mq/client.js.map +1 -1
- package/dist/cjs/mq/index.d.ts +1 -0
- package/dist/cjs/mq/index.js +3 -1
- package/dist/cjs/mq/index.js.map +1 -1
- package/dist/cjs/mq/topic.d.ts +29 -0
- package/dist/cjs/mq/topic.js +48 -0
- package/dist/cjs/mq/topic.js.map +1 -0
- package/dist/cjs/mq/types.d.ts +21 -1
- package/dist/cjs/mq/types.js.map +1 -1
- package/dist/cjs/openapi/api.generated.d.ts +20 -0
- package/dist/cjs/openapi/api.generated.js +11 -0
- package/dist/cjs/openapi/api.generated.js.map +1 -1
- package/dist/cjs/openapi/client.d.ts +46 -0
- package/dist/cjs/openapi/client.js +161 -12
- package/dist/cjs/openapi/client.js.map +1 -1
- package/dist/cjs/openapi/codecs.generated.d.ts +74 -0
- package/dist/cjs/openapi/codecs.generated.js +1 -1
- package/dist/cjs/openapi/codecs.generated.js.map +1 -1
- package/dist/cjs/openapi/types.generated.d.ts +205 -1
- package/dist/cjs/openapi/types.generated.js.map +1 -1
- package/dist/cjs/page-runtime.d.ts +48 -0
- package/dist/cjs/page-runtime.js +74 -0
- package/dist/cjs/page-runtime.js.map +1 -0
- package/dist/cjs/platform-proxy.d.ts +17 -0
- package/dist/cjs/platform-proxy.js +52 -0
- package/dist/cjs/platform-proxy.js.map +1 -0
- package/dist/cjs/runtime-context.d.ts +2 -1
- package/dist/cjs/runtime-context.js +6 -2
- package/dist/cjs/runtime-context.js.map +1 -1
- package/dist/cjs/runtime-env.js.map +1 -1
- package/dist/cjs/source.d.ts +19 -1
- package/dist/cjs/source.js +9 -3
- package/dist/cjs/source.js.map +1 -1
- package/dist/cjs/stream.js +3 -2
- package/dist/cjs/stream.js.map +1 -1
- package/dist/cjs/streams/contract.generated.d.ts +7 -1
- package/dist/cjs/streams/contract.generated.js.map +1 -1
- package/dist/cjs/streams/messages.js +1 -10
- package/dist/cjs/streams/messages.js.map +1 -1
- package/dist/cjs/streams/requests.d.ts +3 -0
- package/dist/cjs/streams/requests.js +5 -2
- package/dist/cjs/streams/requests.js.map +1 -1
- package/dist/cjs/streams/transport.d.ts +1 -13
- package/dist/cjs/streams/transport.js +2 -18
- package/dist/cjs/streams/transport.js.map +1 -1
- package/dist/cjs/uns-v2/client.d.ts +1 -6
- package/dist/cjs/uns-v2/client.js +33 -8
- package/dist/cjs/uns-v2/client.js.map +1 -1
- package/dist/cjs/uns-v2/data-client.d.ts +5 -2
- package/dist/cjs/uns-v2/data-client.js +7 -4
- package/dist/cjs/uns-v2/data-client.js.map +1 -1
- package/dist/cjs/uns-v2/generated.d.ts +6783 -4655
- package/dist/cjs/uns-v2/generated.js +221 -65
- package/dist/cjs/uns-v2/generated.js.map +1 -1
- package/dist/cjs/uns-v2/sse.d.ts +7 -1
- package/dist/cjs/uns-v2/sse.js +12 -2
- package/dist/cjs/uns-v2/sse.js.map +1 -1
- package/dist/esm/client.d.ts +79 -3
- package/dist/esm/client.js +5 -7
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cloud-server.d.ts +33 -26
- package/dist/esm/cloud-server.js +99 -97
- package/dist/esm/cloud-server.js.map +1 -1
- package/dist/esm/cloud-transport.d.ts +15 -20
- package/dist/esm/cloud-transport.js +86 -67
- package/dist/esm/cloud-transport.js.map +1 -1
- package/dist/esm/connection-diagnostics.d.ts +2 -0
- package/dist/esm/connection-diagnostics.js +17 -0
- package/dist/esm/connection-diagnostics.js.map +1 -0
- package/dist/esm/data/bootstrap.js +3 -4
- package/dist/esm/data/bootstrap.js.map +1 -1
- package/dist/esm/data/call.d.ts +1 -3
- package/dist/esm/data/call.js +9 -13
- package/dist/esm/data/call.js.map +1 -1
- package/dist/esm/data/client.d.ts +8 -7
- package/dist/esm/data/client.js +22 -9
- package/dist/esm/data/client.js.map +1 -1
- package/dist/esm/data/collection.d.ts +3 -5
- package/dist/esm/data/collection.js +3 -4
- package/dist/esm/data/collection.js.map +1 -1
- package/dist/esm/data/context.d.ts +5 -11
- package/dist/esm/data/context.js +19 -33
- package/dist/esm/data/context.js.map +1 -1
- package/dist/esm/data/error.d.ts +1 -1
- package/dist/esm/data/error.js +3 -3
- package/dist/esm/data/error.js.map +1 -1
- package/dist/esm/data/index.d.ts +5 -3
- package/dist/esm/data/index.js +3 -2
- package/dist/esm/data/index.js.map +1 -1
- package/dist/esm/data/relay.d.ts +51 -0
- package/dist/esm/data/relay.js +368 -0
- package/dist/esm/data/relay.js.map +1 -0
- package/dist/esm/data/subscribe.d.ts +22 -38
- package/dist/esm/data/subscribe.js +22 -27
- package/dist/esm/data/subscribe.js.map +1 -1
- package/dist/esm/event-stream.d.ts +28 -0
- package/dist/esm/event-stream.js +217 -0
- package/dist/esm/event-stream.js.map +1 -0
- package/dist/esm/mq/client.d.ts +32 -1
- package/dist/esm/mq/client.js +404 -69
- package/dist/esm/mq/client.js.map +1 -1
- package/dist/esm/mq/index.d.ts +1 -0
- package/dist/esm/mq/index.js +1 -0
- package/dist/esm/mq/index.js.map +1 -1
- package/dist/esm/mq/topic.d.ts +29 -0
- package/dist/esm/mq/topic.js +44 -0
- package/dist/esm/mq/topic.js.map +1 -0
- package/dist/esm/mq/types.d.ts +21 -1
- package/dist/esm/mq/types.js.map +1 -1
- package/dist/esm/openapi/api.generated.d.ts +20 -0
- package/dist/esm/openapi/api.generated.js +11 -0
- package/dist/esm/openapi/api.generated.js.map +1 -1
- package/dist/esm/openapi/client.d.ts +46 -0
- package/dist/esm/openapi/client.js +160 -12
- package/dist/esm/openapi/client.js.map +1 -1
- package/dist/esm/openapi/codecs.generated.d.ts +74 -0
- package/dist/esm/openapi/codecs.generated.js +1 -1
- package/dist/esm/openapi/codecs.generated.js.map +1 -1
- package/dist/esm/openapi/types.generated.d.ts +205 -1
- package/dist/esm/openapi/types.generated.js.map +1 -1
- package/dist/esm/page-runtime.d.ts +48 -0
- package/dist/esm/page-runtime.js +65 -0
- package/dist/esm/page-runtime.js.map +1 -0
- package/dist/esm/platform-proxy.d.ts +17 -0
- package/dist/esm/platform-proxy.js +47 -0
- package/dist/esm/platform-proxy.js.map +1 -0
- package/dist/esm/runtime-context.d.ts +2 -1
- package/dist/esm/runtime-context.js +6 -2
- package/dist/esm/runtime-context.js.map +1 -1
- package/dist/esm/runtime-env.js.map +1 -1
- package/dist/esm/source.d.ts +19 -1
- package/dist/esm/source.js +10 -4
- package/dist/esm/source.js.map +1 -1
- package/dist/esm/stream.js +4 -3
- package/dist/esm/stream.js.map +1 -1
- package/dist/esm/streams/contract.generated.d.ts +7 -1
- package/dist/esm/streams/contract.generated.js.map +1 -1
- package/dist/esm/streams/messages.js +1 -10
- package/dist/esm/streams/messages.js.map +1 -1
- package/dist/esm/streams/requests.d.ts +3 -0
- package/dist/esm/streams/requests.js +5 -2
- package/dist/esm/streams/requests.js.map +1 -1
- package/dist/esm/streams/transport.d.ts +1 -13
- package/dist/esm/streams/transport.js +1 -17
- package/dist/esm/streams/transport.js.map +1 -1
- package/dist/esm/uns-v2/client.d.ts +1 -6
- package/dist/esm/uns-v2/client.js +33 -8
- package/dist/esm/uns-v2/client.js.map +1 -1
- package/dist/esm/uns-v2/data-client.d.ts +5 -2
- package/dist/esm/uns-v2/data-client.js +8 -5
- package/dist/esm/uns-v2/data-client.js.map +1 -1
- package/dist/esm/uns-v2/generated.d.ts +6783 -4655
- package/dist/esm/uns-v2/generated.js +221 -65
- package/dist/esm/uns-v2/generated.js.map +1 -1
- package/dist/esm/uns-v2/sse.d.ts +7 -1
- package/dist/esm/uns-v2/sse.js +12 -2
- package/dist/esm/uns-v2/sse.js.map +1 -1
- package/package.json +25 -12
- package/skills/tier0-sdk-v2/SKILL.md +13 -5
- package/skills/tier0-sdk-v2/references/integration.md +14 -24
- package/skills/tier0-sdk-v2/references/local-connection.md +61 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/SKILL.md +11 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/consistency.md +8 -12
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/errors.md +24 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/modeling.md +3 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/read-write.md +46 -13
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/schema-evolution.md +2 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/SKILL.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/references/app-members.md +5 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/SKILL.md +25 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/references/quickstart.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/SKILL.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/references/send.md +6 -6
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/SKILL.md +4 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/changes.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/create.md +26 -36
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/delete.md +9 -23
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/protocols.md +12 -29
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/read.md +4 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/update.md +5 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/SKILL.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/create.md +23 -9
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/deploy.md +14 -12
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/http-server.md +4 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/read.md +4 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-streams/SKILL.md +12 -12
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/SKILL.md +3 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/create.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/delete.md +47 -20
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/namespace.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/read.md +11 -6
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/restore.md +12 -13
- package/dist/cjs/app-identity-contract.generated.d.ts +0 -22
- package/dist/cjs/app-identity-contract.generated.js +0 -51
- package/dist/cjs/app-identity-contract.generated.js.map +0 -1
- package/dist/cjs/runtime.d.ts +0 -13
- package/dist/cjs/runtime.js +0 -51
- package/dist/cjs/runtime.js.map +0 -1
- package/dist/esm/app-identity-contract.generated.d.ts +0 -22
- package/dist/esm/app-identity-contract.generated.js +0 -48
- package/dist/esm/app-identity-contract.generated.js.map +0 -1
- package/dist/esm/runtime.d.ts +0 -13
- package/dist/esm/runtime.js +0 -46
- package/dist/esm/runtime.js.map +0 -1
package/dist/esm/uns-v2/sse.d.ts
CHANGED
|
@@ -14,7 +14,13 @@ export interface ServerSentEvent {
|
|
|
14
14
|
export declare const SSE_MAX_LINE_BYTES: number;
|
|
15
15
|
export declare class ServerSentEventsError extends Error {
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Yields each dispatched event and each comment line, plus `{ id }` for a frame
|
|
19
|
+
* that only moves the position; `retry:` is ignored. Throws
|
|
20
|
+
* ServerSentEventsError on an oversized line.
|
|
21
|
+
*/
|
|
18
22
|
export declare function parseServerSentEvents(stream: ReadableStream<Uint8Array>): AsyncGenerator<ServerSentEvent | {
|
|
19
23
|
comment: string;
|
|
24
|
+
} | {
|
|
25
|
+
id: string;
|
|
20
26
|
}>;
|
package/dist/esm/uns-v2/sse.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
export const SSE_MAX_LINE_BYTES = 1 << 20;
|
|
8
8
|
export class ServerSentEventsError extends Error {
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Yields each dispatched event and each comment line, plus `{ id }` for a frame
|
|
12
|
+
* that only moves the position; `retry:` is ignored. Throws
|
|
13
|
+
* ServerSentEventsError on an oversized line.
|
|
14
|
+
*/
|
|
11
15
|
export async function* parseServerSentEvents(stream) {
|
|
12
16
|
const reader = stream.getReader();
|
|
13
17
|
const decoder = new TextDecoder();
|
|
@@ -15,6 +19,7 @@ export async function* parseServerSentEvents(stream) {
|
|
|
15
19
|
let event = '';
|
|
16
20
|
let data = [];
|
|
17
21
|
let id;
|
|
22
|
+
let moved = false;
|
|
18
23
|
try {
|
|
19
24
|
while (true) {
|
|
20
25
|
const { done, value } = await reader.read();
|
|
@@ -29,6 +34,9 @@ export async function* parseServerSentEvents(stream) {
|
|
|
29
34
|
if (line === '') {
|
|
30
35
|
if (data.length > 0 || event)
|
|
31
36
|
yield { event: event || 'message', data: data.join('\n'), ...(id === undefined ? {} : { id }) };
|
|
37
|
+
else if (moved && id !== undefined)
|
|
38
|
+
yield { id };
|
|
39
|
+
moved = false;
|
|
32
40
|
event = '';
|
|
33
41
|
data = [];
|
|
34
42
|
continue;
|
|
@@ -45,8 +53,10 @@ export async function* parseServerSentEvents(stream) {
|
|
|
45
53
|
event = text;
|
|
46
54
|
else if (field === 'data')
|
|
47
55
|
data.push(text);
|
|
48
|
-
else if (field === 'id' && !text.includes('\u0000'))
|
|
56
|
+
else if (field === 'id' && !text.includes('\u0000')) {
|
|
49
57
|
id = text;
|
|
58
|
+
moved = true;
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
if (done)
|
|
52
62
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../../src/uns-v2/sse.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,2FAA2F;AAC3F,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,CAAC;AAE1C,MAAM,OAAO,qBAAsB,SAAQ,KAAK;CAAG;AAEnD
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../../src/uns-v2/sse.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal `text/event-stream` parser (WHATWG server-sent events). Unlike\n * EventSource it also dispatches a named event without data lines, and\n * reports comment lines, so a keep-alive in either form is visible.\n */\n\nexport interface ServerSentEvent {\n /** `message` when the server names no event. */\n event: string;\n data: string;\n /** Last `id:` seen on the stream so far, as EventSource keeps it. */\n id?: string;\n}\n\n/** A line longer than this is not an event stream (e.g. a proxy page without newlines). */\nexport const SSE_MAX_LINE_BYTES = 1 << 20;\n\nexport class ServerSentEventsError extends Error {}\n\n/**\n * Yields each dispatched event and each comment line, plus `{ id }` for a frame\n * that only moves the position; `retry:` is ignored. Throws\n * ServerSentEventsError on an oversized line.\n */\nexport async function* parseServerSentEvents(stream: ReadableStream<Uint8Array>): AsyncGenerator<ServerSentEvent | { comment: string } | { id: string }> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let event = '';\n let data: string[] = [];\n let id: string | undefined;\n let moved = false;\n try {\n while (true) {\n const { done, value } = await reader.read();\n buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n // A trailing \"\\r\" may be the first half of \"\\r\\n\": keep it for the next chunk.\n const lines = buffer.split(/\\r\\n|\\n|\\r(?!$)/);\n buffer = done ? '' : (lines.pop() ?? '');\n // UTF-16 length bounds the UTF-8 size from below; good enough to stop runaway input.\n if (buffer.length > SSE_MAX_LINE_BYTES) throw new ServerSentEventsError('Server-sent event line too long.');\n for (const line of lines) {\n if (line === '') {\n if (data.length > 0 || event) yield { event: event || 'message', data: data.join('\\n'), ...(id === undefined ? {} : { id }) };\n else if (moved && id !== undefined) yield { id };\n moved = false;\n event = '';\n data = [];\n continue;\n }\n if (line.startsWith(':')) {\n yield { comment: line.slice(1).trimStart() };\n continue;\n }\n const colon = line.indexOf(':');\n const field = colon < 0 ? line : line.slice(0, colon);\n const raw = colon < 0 ? '' : line.slice(colon + 1);\n const text = raw.startsWith(' ') ? raw.slice(1) : raw;\n if (field === 'event') event = text;\n else if (field === 'data') data.push(text);\n else if (field === 'id' && !text.includes('\\u0000')) {\n id = text;\n moved = true;\n }\n }\n if (done) return;\n }\n } finally {\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tier0/sdk-v2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Tier0 Cloud new-architecture SDK for TypeScript and JavaScript",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -97,6 +97,26 @@
|
|
|
97
97
|
"default": "./dist/cjs/mq/index.js"
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
|
+
"./page-runtime": {
|
|
101
|
+
"import": {
|
|
102
|
+
"types": "./dist/esm/page-runtime.d.ts",
|
|
103
|
+
"default": "./dist/esm/page-runtime.js"
|
|
104
|
+
},
|
|
105
|
+
"require": {
|
|
106
|
+
"types": "./dist/cjs/page-runtime.d.ts",
|
|
107
|
+
"default": "./dist/cjs/page-runtime.js"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"./event-stream": {
|
|
111
|
+
"import": {
|
|
112
|
+
"types": "./dist/esm/event-stream.d.ts",
|
|
113
|
+
"default": "./dist/esm/event-stream.js"
|
|
114
|
+
},
|
|
115
|
+
"require": {
|
|
116
|
+
"types": "./dist/cjs/event-stream.d.ts",
|
|
117
|
+
"default": "./dist/cjs/event-stream.js"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
100
120
|
"./server": {
|
|
101
121
|
"import": {
|
|
102
122
|
"types": "./dist/esm/cloud-server.d.ts",
|
|
@@ -152,16 +172,6 @@
|
|
|
152
172
|
}
|
|
153
173
|
},
|
|
154
174
|
"./package.json": "./package.json",
|
|
155
|
-
"./runtime": {
|
|
156
|
-
"import": {
|
|
157
|
-
"types": "./dist/esm/runtime.d.ts",
|
|
158
|
-
"default": "./dist/esm/runtime.js"
|
|
159
|
-
},
|
|
160
|
-
"require": {
|
|
161
|
-
"types": "./dist/cjs/runtime.d.ts",
|
|
162
|
-
"default": "./dist/cjs/runtime.js"
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
175
|
"./streams": {
|
|
166
176
|
"import": {
|
|
167
177
|
"types": "./dist/esm/streams/index.d.ts",
|
|
@@ -175,13 +185,15 @@
|
|
|
175
185
|
},
|
|
176
186
|
"scripts": {
|
|
177
187
|
"sync:contracts": "node scripts/sync-contracts.mjs",
|
|
188
|
+
"sync:source-stream-contract": "node scripts/sync-source-stream-contract.mjs",
|
|
178
189
|
"generate": "node scripts/generate-openapi.mjs && node scripts/generate-uns-v2.mjs",
|
|
179
|
-
"check:contracts": "node scripts/check-contracts.mjs && npm run check:acquisition-contract && node scripts/sync-capability-contract.mjs --check && node --test scripts/retired-stream-probe.test.mjs scripts/retired-http-push.test.mjs scripts/retired-controls.test.mjs scripts/retired-public-labels.test.mjs scripts/platform-schema-projection.test.mjs scripts/data-capability-vector.test.mjs scripts/sync-contracts.test.mjs scripts/sync-capability-contract.test.mjs scripts/live-program-contract.test.mjs scripts/sync-http-run-observations.test.mjs && node scripts/generate-openapi.mjs --check && node scripts/generate-uns-v2.mjs --check && npm run check:stream-contract",
|
|
190
|
+
"check:contracts": "node scripts/check-contracts.mjs && npm run check:acquisition-contract && node scripts/sync-capability-contract.mjs --check && node --test scripts/retired-stream-probe.test.mjs scripts/retired-http-push.test.mjs scripts/retired-controls.test.mjs scripts/retired-public-labels.test.mjs scripts/platform-schema-projection.test.mjs scripts/data-capability-vector.test.mjs scripts/sync-contracts.test.mjs scripts/sync-capability-contract.test.mjs scripts/live-program-contract.test.mjs scripts/sync-http-run-observations.test.mjs scripts/member-list-proxy-guidance.test.mjs && node scripts/generate-openapi.mjs --check && node scripts/generate-uns-v2.mjs --check && npm run check:stream-contract",
|
|
180
191
|
"check:skills": "node scripts/check-skills.mjs && node scripts/check-skill-examples.mjs",
|
|
181
192
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
182
193
|
"build": "node scripts/build.mjs",
|
|
183
194
|
"prepare": "npm run build",
|
|
184
195
|
"test": "vitest run",
|
|
196
|
+
"test:gateway-enforce": "node scripts/test-gateway-enforce.mjs",
|
|
185
197
|
"check": "npm run check:contracts && npm run check:skills && npm run typecheck && npm test && npm run build && node scripts/check-package.mjs",
|
|
186
198
|
"prepack": "node scripts/sync-stream-contract.mjs --check --committed && node scripts/check-contracts.mjs --committed && npm run check:contracts && npm run check:skills && npm run build",
|
|
187
199
|
"prepublishOnly": "node scripts/sync-stream-contract.mjs --check --committed && node scripts/check-contracts.mjs --committed && npm run check",
|
|
@@ -226,6 +238,7 @@
|
|
|
226
238
|
"ajv": "^8.20.0",
|
|
227
239
|
"ajv-formats": "^3.0.1",
|
|
228
240
|
"esbuild": "^0.25.0",
|
|
241
|
+
"mqtt-v530": "npm:mqtt@5.3.0",
|
|
229
242
|
"openapi-typescript": "^7.0.0",
|
|
230
243
|
"react": "^19.0.0",
|
|
231
244
|
"typescript": "~5.9.3",
|
|
@@ -27,19 +27,27 @@ description: 使用 @tier0/sdk-v2 开发或迁移 Tier0 云端 TypeScript/JavaSc
|
|
|
27
27
|
|
|
28
28
|
## 初始化与身份
|
|
29
29
|
|
|
30
|
+
个人 Key 可直接读取已有集合,无需创建平台 App;网关配置、运行示例与排错见[本地连接指南](references/local-connection.md)。
|
|
31
|
+
|
|
30
32
|
```ts
|
|
31
33
|
import { createClient } from '@tier0/sdk-v2';
|
|
32
34
|
|
|
33
|
-
//
|
|
35
|
+
// 平台注入 TIER0_API_HOST 和 App 的 Key TIER0_API_KEY;本地开发自己设这两个。
|
|
34
36
|
export const client = createClient({ transportTimeoutMs: 30_000 });
|
|
35
37
|
export async function inspectConnection() {
|
|
36
38
|
return client.system.info();
|
|
37
39
|
}
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
也可传 `apiHost`、`apiKey` 或 `getApiHost` / `getApiKey` 回调。API Key 留在服务端;不要生成 `VITE_*KEY` 或让页面携带 App Key。浏览器通过自己的服务端接口访问平台;App Stream
|
|
42
|
+
也可传 `apiHost`、`apiKey` 或 `getApiHost` / `getApiKey` 回调。API Key 留在服务端;不要生成 `VITE_*KEY` 或让页面携带 App Key。浏览器通过自己的服务端接口访问平台;App Stream 交互(`client.streams`)也只在服务端用带 Key 的客户端。消息要求 Node.js 22+(WebSocket)。
|
|
43
|
+
|
|
44
|
+
凭证来源按属性存在性选择,优先顺序为 `apiKeyProvider`、`apiKey`、`getApiKey`;只有都未提供才读取 `TIER0_API_KEY`。显式 `undefined` 或 getter 返回 `undefined` 不会回退到环境 Key。每个用户请求使用独立客户端,不要把全局 `configureClient` 当作请求身份。
|
|
45
|
+
|
|
46
|
+
**Tier0 预览和部署里,App 的 Key 在运行进程的环境里**:平台注入 `TIER0_API_KEY`(这个 App 的 Key,秘密;预览是 preview Key,部署是 deployed Key)和 `TIER0_API_HOST`,`createClient()` 直接用它们,只发一个 `X-API-Key`。App 代码不要自己读、复制、打印或下发这个 Key,也不要生成或硬编码 Key;直接调平台时也通过 SDK。身份读平台设置的 `TIER0_WORKSPACE_ID`、`TIER0_APP_ID`。
|
|
47
|
+
|
|
48
|
+
App 的 Key 只能调 `/openapi/v2/uns/**`、`assets/files/**`(`client.files`;`assets/upload` 不在内)、`notifications/send`、`platform/getMembers`(`members.listWorkspace()`)、`apps/access-members/list`(`members.listApp()`)、`stream-requests/discover` 和 `stream-requests/<code>/describe|invoke`、`stream-messages/connect`(`client.streams`)、`capabilities/discover`、`info`;其他接口(`system.whoami()`、`notifications.get`、`mqttAuth`、`agent`)回 403 `hosted_app_key_route_forbidden`;Source / Stream 管理接口 App 的 Key 无权调用,Backend 回 403。App 的平台能力固定 11 项(`openapi.base`、`platform.users.read`、`notifications.send`、文件上传 / 下载 / 删除、`stream.requests.read` / `invoke` / `write`、`stream.messages.publish` / `subscribe`),每个 App 都有、不在 App 设置里配置,生效的是它们和 Key 权限、Key 模式、工作区套餐的交集。外部集合只由工作区 Owner/Admin 在 App 设置里授权,删掉后下一个请求起生效。MQTT 主题只能用 App 自己的命名空间 `apps/<TIER0_APP_ID>/<TIER0_RUNTIME>/…`,用 `appTopic()` 拼,见 [MQTT](tier0-sdk-v2-mq/SKILL.md)。没有 `TIER0_API_KEY` 而 `TIER0_RUNTIME` 为 `deployed` / `preview` 时 SDK 进入平台代理模式(不发凭据、带 `X-Tier0-SDK: 1`),当前平台不会出现这种运行时,见 [错误码](tier0-sdk-v2-data/references/errors.md#平台代理模式的错误)。
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
不同身份使用独立实例。App 调平台只有一种身份:App 自身。当前用户和角色从前门注入的 `X-Tier0-Visitor` 读(`parseVisitorHeader`),按角色决定谁能看、能改什么由 App 在服务端自己做,见 [身份与框架集成](references/integration.md)。
|
|
43
51
|
|
|
44
52
|
## 返回值与错误
|
|
45
53
|
|
|
@@ -52,7 +60,7 @@ export async function inspectConnection() {
|
|
|
52
60
|
|
|
53
61
|
方法存在不等于目标环境已部署或当前 Key 有权限。整数可能为 `bigint`;UNS 任意 JSON 中还可能含 `UnsV2RawNumber`。不要将标识符强制转为 Number;给浏览器返回数据时显式定义精确数字的序列化格式。
|
|
54
62
|
|
|
55
|
-
`createClient()` 的 HTTP
|
|
63
|
+
`createClient()` 的 HTTP 不自动重试,唯一例外是平台代理模式下代理自己产生的 `503` / `429`(带 `X-Tier0-Proxy: not-forwarded`,请求没有到达平台),SDK 按 `retry_after_ms` 最多重试 3 次;平台返回的 5xx 不重试。UNS 错误的 `execution` 说明执行到哪一步、`retry.safe` 说明能否用同一请求重发;带 `outcomeUnknown` 的失败应先对账,保留原意图键;取消请求不代表服务端撤销。App 数据层(`@tier0/sdk-v2/data`)会按 `retry.safe` 自动重试,见 App 数据 Skill。Source/Stream 仍使用底层 UNS v2 契约,所以错误类名保留 UNS,不代表它们仍属于数据入口。
|
|
56
64
|
|
|
57
65
|
## 从首版入口迁移
|
|
58
66
|
|
|
@@ -63,7 +71,7 @@ export async function inspectConnection() {
|
|
|
63
71
|
- `client.uns.recordFind(body)` 保持不变。
|
|
64
72
|
- 低层 `createUnsV2Client` 保留完整生成契约,供契约工具使用;业务主入口不再把 Source/Stream 管理混入 `client.uns`。
|
|
65
73
|
|
|
66
|
-
React/Vue
|
|
74
|
+
访问者身份与 React/Vue 示例见 [集成参考](references/integration.md)。完成接入时验证目标入口、真实身份、响应解包和错误分支;SDK 本地测试通过不代表环境联调完成。
|
|
67
75
|
|
|
68
76
|
Stream 请求与消息交互统一使用本包 `client.streams.requests` / `client.streams.messages`;[交互说明](tier0-sdk-v2-streams/SKILL.md)。Scaffold 只依赖本包;`client.stream` 管理接口与运行时交互用途不同。
|
|
69
77
|
|
|
@@ -2,36 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
仅在接入服务端身份或 React/Vue 时阅读本文件。基本分域调用见入口 Skill。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 访问者身份(X-Tier0-Visitor)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
平台里 App 的每个请求都经过前门:前门先剥掉客户端带来的全部 `X-Tier0-*`,再注入明文的 `X-Tier0-Visitor`(UTF-8 JSON 对象的无填充 base64url,不签名)。服务端用 `parseVisitorHeader` 解析,它严格校验字段、类型、长度,并要求 `app` 等于 `TIER0_APP_ID`:
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
import { parseVisitorHeader, VISITOR_HEADER, VisitorError, type Visitor } from '@tier0/sdk-v2/server';
|
|
11
|
+
|
|
12
|
+
export function visitorOf(request: Request): Visitor | null {
|
|
13
|
+
try {
|
|
14
|
+
return parseVisitorHeader(request.headers.get(VISITOR_HEADER));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof VisitorError && error.code === 'VISITOR_HEADER_INVALID') return null; // 回 401
|
|
17
|
+
throw error; // VISITOR_CONFIGURATION_INVALID:没有 TIER0_APP_ID,回 503
|
|
18
|
+
}
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
返回 `{ subType: 'employee' | 'machine', subject, grant, workspaceID, appID, runtime: 'deployed' | 'preview', appRoles, activeRole? }`。`appRoles` 是访问者在 App 里的角色(预览里是「以某角色查看」选中的角色),`activeRole` 只在预览的「以某角色查看」里出现,只用于展示。
|
|
23
23
|
|
|
24
|
-
App
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import { createClient } from '@tier0/sdk-v2';
|
|
28
|
-
|
|
29
|
-
export function clientForTrustedGatewayRequest(apiHost: string, apiKey: string, trustedDataSubject: string) {
|
|
30
|
-
return createClient({ apiHost, apiKey, unsV2AppDataSubject: trustedDataSubject });
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
SDK 仅检查委托的紧凑格式,不自行信任/验证其 claims。不要直接转发公开入口任意同名 header,部署边界必须保证它来自可信网关。不要跨员工复用这个实例或令牌。
|
|
24
|
+
App 调平台只有一种身份:App 自身(App Key,平台注入在运行进程的 `TIER0_API_KEY` 里,SDK 直接用)。平台不按访问者收窄 App 的数据,按角色决定当前用户能看、能改哪些记录是 App 自己的事:在服务端按 `appRoles` 过滤查询条件、拒绝写入,返回给浏览器前裁掉不该看的字段。不要把访问者 ID 或角色当成平台调用的参数去「代表」用户。
|
|
35
25
|
|
|
36
26
|
## React
|
|
37
27
|
|
|
@@ -69,4 +59,4 @@ export function useRecords(client: Tier0Client) {
|
|
|
69
59
|
|
|
70
60
|
## 联调证据
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
访问者头的字段以平台前门实际注入的 `X-Tier0-Visitor` 为准。契约生成、类型与本地测试证明包内部一致,不替代环境联调。确认身份、权限与字段返回后再切换真实业务流量。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 本地连接与排错
|
|
2
|
+
|
|
3
|
+
个人 Key 可用于本地脚本、数据分析和调试。SDK 与 MCP 是两种调用入口;MCP 已连接不代表本地 SDK 已配置地址或凭证。读取已有普通 collection 不要求创建或部署平台 App,也不需要 App ID。实际访问范围由个人 Key 及其所属用户的权限决定。
|
|
4
|
+
|
|
5
|
+
## 确定 API 地址
|
|
6
|
+
|
|
7
|
+
`apiHost` / `TIER0_API_HOST` 填写当前环境的 **OpenAPI 网关基础地址**,不附加 `/openapi/v2`、页面路径或 Workspace ID;SDK 自动添加接口路径。平台网页地址、MCP 地址与网关地址不能互换。
|
|
8
|
+
|
|
9
|
+
| 已知平台环境 | OpenAPI 网关基础地址 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 阿里云 ACK 开发环境 `https://dev.tier0.cn` | `https://api-dev.tier0.cn` |
|
|
12
|
+
|
|
13
|
+
其他环境或私有化部署:从该环境的部署/接入指南取得 OpenAPI 公网入口;若指南未提供,向环境管理员确认“对本机可达、暴露 `/openapi/v2` 的网关基础地址”。不要靠网页域名拼接或猜测地址。若没有对外暴露网关,需由管理员提供入口或网络接入方式,SDK 无法补齐部署。
|
|
14
|
+
|
|
15
|
+
私有化地址通过 `apiHost` 或环境变量显式配置,支持已有的基础路径前缀。本地 HTTP 网关需写明 `http://`,裸域名默认 HTTPS。SDK 不从 Key 推断地址,不探测候选域名、不自动切换 Host,也不跟随 API 重定向传递密钥。
|
|
16
|
+
|
|
17
|
+
## 个人 Key 读取已有集合
|
|
18
|
+
|
|
19
|
+
安装 SDK,使用 Node.js 20.19+。在本机创建 `.env.local`,将它加入 `.gitignore`,填入已经确认的网关、个人 Key 和 collection ID。密钥只留在本地受控配置或服务端,不写进代码、浏览器变量、日志或提交记录。
|
|
20
|
+
|
|
21
|
+
```dotenv
|
|
22
|
+
TIER0_API_HOST=https://api-dev.tier0.cn
|
|
23
|
+
TIER0_API_KEY=填入该环境的个人Key
|
|
24
|
+
TIER0_COLLECTION_ID=填入已有collection的ID
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
将以下代码保存为 `read-records.mjs`,按已知集合 ID 直接读取。
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createClient } from '@tier0/sdk-v2';
|
|
31
|
+
|
|
32
|
+
const collectionId = process.env.TIER0_COLLECTION_ID;
|
|
33
|
+
if (!collectionId) throw new Error('请设置 TIER0_COLLECTION_ID');
|
|
34
|
+
const client = createClient();
|
|
35
|
+
const result = await client.uns.recordFind({ collectionId, limit: 20 });
|
|
36
|
+
console.dir(result.data, { depth: null });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
node --env-file=.env.local read-records.mjs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
示例只读第一页,后续按 `pageInfo` 分页;不会创建集合、修改 Schema 或写入种子数据。collection 由 MCP、页面或其他应用创建不影响此调用方式,知道 ID 不代表取得权限。长期团队应用使用独立 App 身份,避免依赖某个员工的个人 Key。
|
|
44
|
+
|
|
45
|
+
个人 Key 也可通过数据层的 `data.collection({ collectionId })` 引用已有集合、跳过 ensure(见[已有集合用法](../tier0-sdk-v2-data/references/modeling.md));`defineCollection` 声明集合的自动 ensure 需要平台签发的 App Key。
|
|
46
|
+
|
|
47
|
+
## 按响应排错
|
|
48
|
+
|
|
49
|
+
| 响应或错误 | 下一步 |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| HTML / 非 JSON 404、405 | 核对网关基础地址及 `/openapi/v2` 路由;不要先更换 Key。网页域名可能只提供前端页面。 |
|
|
52
|
+
| 401 | 先确认目标网关,再检查 Key 是否提供、是否有效、是否属于该环境;网关前的代理也可能要求认证。 |
|
|
53
|
+
| 403 | 检查当前凭证的资源/操作权限与网关访问限制;不等同于 Key 无效。 |
|
|
54
|
+
| 502、503、504 | 检查网关和上游服务健康;重发写入前确认执行结果,避免重复写入。 |
|
|
55
|
+
| 200 但返回 HTML / `SDK_PROTOCOL` | 核对网关地址、代理转发及响应格式;页面可打开不代表 OpenAPI 可用。 |
|
|
56
|
+
| 网络错误或超时 | 检查 DNS、网络接入、TLS 和服务连通性;不能仅据此判断凭证无效。 |
|
|
57
|
+
| 正常 JSON 业务错误 | 按服务端错误码、权限信息和请求 ID 排查;JSON 404 也可能表示业务资源不存在。 |
|
|
58
|
+
|
|
59
|
+
身份或 Workspace 不明确时,可单独调用 `client.system.whoami()` 核对返回的 `workspaceID`;它不是读取记录的前置步骤。无密钥请求返回 401 只证明已到达认证入口,不能证明某个 Key 有权读取目标集合。
|
|
60
|
+
|
|
61
|
+
错误提示不回显 Key、配置 URL 或原始 HTML;SDK 保留原有错误类型、HTTP 状态和执行/重试语义,不自动更换凭证或重试。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tier0-sdk-v2-data
|
|
3
|
-
description: 使用 @tier0/sdk-v2/data 在 App 服务端声明并读写本 App 的 UNS 集合:Zod 建模、种子与 TTL、Mongo
|
|
3
|
+
description: 使用 @tier0/sdk-v2/data 在 App 服务端声明并读写本 App 的 UNS 集合:Zod 建模、种子与 TTL、Mongo 风格读写、带总数分页与分组计数、原子批量与事务、变更订阅与转发给浏览器、Schema 演进和错误码处理。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# App 数据层
|
|
@@ -9,9 +9,15 @@ description: 使用 @tier0/sdk-v2/data 在 App 服务端声明并读写本 App
|
|
|
9
9
|
|
|
10
10
|
`@tier0/sdk-v2/data` 是 App 业务数据的服务端入口:用 `defineCollection` 声明集合,`createDataClient` 在首次访问时一次性 ensure 全部声明,然后用 Mongo 驱动风格的集合句柄读写 UNS 记录。没有 SQL、ORM、连接串或迁移步骤;Preview 与部署后的 App 共用同一批集合和记录。
|
|
11
11
|
|
|
12
|
+
所有数据调用都是 App 自身:平台不按当前用户收窄。按用户角色决定谁能看、能改哪些记录由 App 在服务端自己做,当前用户和角色从访问者头读(`@tier0/sdk-v2/server` 的 `parseVisitorHeader`,见[身份与框架集成](../references/integration.md))。
|
|
13
|
+
|
|
12
14
|
这里创建的集合以 `ownerAppId` 标识唯一所属 App。使用已有或其他 App 共享的集合时,用 `data.collection({ collectionId })` 获取句柄;可选 `schema` 仅做本地写入校验,不创建或修改集合。已有 ID 引用同样用于 `queryBatch`、`transact` 和 `subscribe`,不放入 `createDataClient` 的 `collections` 声明数组,不执行 ensure 或写种子。无需服务端 alias,仍须当前 App 已有相应集合授权;SDK 不授予权限,复用不改变所属关系。底层 [UNS](../tier0-sdk-v2-uns/SKILL.md) 的记录接口仍可直接按 ID 使用。
|
|
13
15
|
|
|
14
|
-
数据读写只在服务端代码(业务 service)里用 `@tier0/sdk-v2/data`:该入口依赖 Node 内置模块,浏览器打包会直接失败。组件和 route 的客户端代码只能从 `@tier0/sdk-v2/data/define` 取集合声明和类型(`defineCollection`、`Doc`、`NewDoc` 等),它不加载数据运行时,可以进浏览器包。需要 `zod` 4。底层调用 SDK 生成的 UNS 操作(`appCollectionsEnsure`、`recordFind`、`recordTransactionExecute`
|
|
16
|
+
数据读写只在服务端代码(业务 service)里用 `@tier0/sdk-v2/data`:该入口依赖 Node 内置模块,浏览器打包会直接失败。组件和 route 的客户端代码只能从 `@tier0/sdk-v2/data/define` 取集合声明和类型(`defineCollection`、`Doc`、`NewDoc` 等),它不加载数据运行时,可以进浏览器包。需要 `zod` 4。底层调用 SDK 生成的 UNS 操作(`appCollectionsEnsure`、`recordFind`、`recordTransactionExecute` 等),Tier0 预览和部署里读平台注入的 TIER0_API_HOST 和 App 的 Key TIER0_API_KEY(种子在 ensure 时以 App 身份写入);本地开发自己设这两个。
|
|
17
|
+
|
|
18
|
+
Data 客户端创建时固定 Host 和 Key(含环境值),不支持 `getApiKey` 或 `apiKeyProvider`;切换身份、Host 或环境凭证时须新建客户端,不能让 bootstrap、重试或订阅状态跨身份复用。
|
|
19
|
+
|
|
20
|
+
服务端已从可信身份链取得某主体的有效 Key 时,可用 `createDataClient({ apiHost, apiKey })` 为该主体创建独立实例,通过 `collection({ collectionId })` 访问其已获授权的现有集合;不要跨主体共享该实例。Key 轮换后创建新实例。声明集合的 ensure/seed 仍由服务端判断凭证是否有权限,不因创建客户端而获得授权。
|
|
15
21
|
|
|
16
22
|
集合声明必须分别填写业务名称 `name` 与稳定程序标识 `alias`,例如 `name: '生产工单'`、`alias: 'production_orders'`。name 是唯一界面名称,没有 displayName 或 alias 回退;ensure 通过 alias 找回原集合,不覆盖用户改名。
|
|
17
23
|
|
|
@@ -20,14 +26,14 @@ description: 使用 @tier0/sdk-v2/data 在 App 服务端声明并读写本 App
|
|
|
20
26
|
| 用户任务 | 参考 |
|
|
21
27
|
| --- | --- |
|
|
22
28
|
| 声明集合、选字段类型、索引、种子数据、文档建模 | [建模](references/modeling.md) |
|
|
23
|
-
| 查询、按 ID 读取、一页一次请求(queryBatch
|
|
24
|
-
|
|
|
29
|
+
| 查询、按 ID 读取、一页一次请求(queryBatch)、分页、聚合、写入、批量与原子性、订阅变更并转发给浏览器 | [读写](references/read-write.md) |
|
|
30
|
+
| 状态流转、并发编辑、跨文档不变量、计数器、请求中止与后台任务 | [一致性与请求上下文](references/consistency.md) |
|
|
25
31
|
| 加字段、改类型、改描述、收窄 Schema | [Schema 演进](references/schema-evolution.md) |
|
|
26
32
|
| DataError 字段、UNS 错误码与处理 | [错误码](references/errors.md) |
|
|
27
33
|
|
|
28
34
|
## 最小用法
|
|
29
35
|
|
|
30
|
-
新 App 没有集合时是正常空态:`uns.dataAppContext({})` 返回 `dataState: "uninitialized"` 和空集合,不创建任何数据。无需先调用 context 或手工激活数据策略;声明真实业务集合后,第一次数据访问会自动初始化。`dataState: "ready"` 也不代表可访问整个工作空间,仍以返回的集合授权为准。引用已有 WMS
|
|
36
|
+
新 App 没有集合时是正常空态:`uns.dataAppContext({})` 返回 `dataState: "uninitialized"` 和空集合,不创建任何数据。无需先调用 context 或手工激活数据策略;声明真实业务集合后,第一次数据访问会自动初始化。`dataState: "ready"` 也不代表可访问整个工作空间,仍以返回的集合授权为准。引用已有 WMS 等外部集合要由工作区 Owner/Admin 在 App 设置里授权(Builder agent 加不了),不能创建同名空集合代替;授权删掉后下一个请求起被拒绝。管理员停用返回 `FORBIDDEN`、reason `APP_DATA_POLICY_DISABLED`,不得自动恢复。
|
|
31
37
|
|
|
32
38
|
```ts
|
|
33
39
|
import { z } from 'zod';
|
|
@@ -39,32 +39,28 @@ export async function nextOrderNumber() {
|
|
|
39
39
|
|
|
40
40
|
`transact` 的结果状态不是 `committed`(`dryRun` 时为 `validated`)会抛 `TRANSACTION_FAILED`,`details.results` 带每一步的错误。
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## 调用身份与请求上下文
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
每次调用都是 App 自身,使用平台注入的 App Key;集合 ensure、种子写入、记录租约、幂等键都只按 App 区分,不按当前用户区分。按用户角色决定谁能看、能改什么由 App 在服务端自己做(当前用户和角色见[身份与框架集成](../../references/integration.md)的访问者头)。
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
请求入口用 `runWithAbortSignal(request.signal, next)` 把请求的中止信号交给数据层:调用方断开时,这个请求还在排队的数据调用不再发出(`DATA_ABORTED`,`notStarted`),进行中的被取消。脱离请求的工作(定时器、队列、发出即不管的后续任务)放进 `runDetached(() => ...)`,不随请求中止。ensure、种子写入和 `data.subscribe` 本来就不受请求中止影响。
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
|
-
import {
|
|
49
|
+
import { runDetached, runWithAbortSignal } from '@tier0/sdk-v2/data';
|
|
50
50
|
|
|
51
51
|
export function handleRequest<T>(request: Request, next: () => Promise<T>): Promise<T> {
|
|
52
|
-
return
|
|
52
|
+
return runWithAbortSignal(request.signal, next);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function scheduleFollowUp(task: () => Promise<void>) {
|
|
56
|
-
setTimeout(() => void
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function servingEmployee(): boolean {
|
|
60
|
-
return currentDataSubject() !== undefined;
|
|
56
|
+
setTimeout(() => void runDetached(task), 1000);
|
|
61
57
|
}
|
|
62
58
|
```
|
|
63
59
|
|
|
64
60
|
## 首次访问与自愈
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
进程内第一次访问声明集合时一次性 ensure 全部声明:缺失的集合会被创建(随后写入种子),更宽的声明会放宽存储,存储永远不会被收窄。ensure 失败不缓存,下一次请求会重试(刚导入的 App 可能短暂返回 `APP_NOT_READY`,数据层先自动重试,仍失败时按 503 回答)。已有 ID 引用 `data.collection({ collectionId })` 跳过 ensure,可按原授权[读取回收集合历史](../../tier0-sdk-v2-uns/references/read.md#回收集合的历史读取)。
|
|
67
63
|
|
|
68
|
-
|
|
64
|
+
声明集合的调用被 `COLLECTION_NOT_FOUND` 或 `COLLECTION_NOT_GRANTED` 拒绝时,数据层重新 ensure(重建并重新写入种子),并在集合 ID 确实变化时把该调用重试一次。每个 client 每 30 秒最多重建一次;已有 ID 引用不触发重建。历史读取成功、`APP_GRANT_REVOKED` 或 `OWNER_APP_TRASHED` 均不触发自愈。延续旧集合游标的分页、`updateMany` / `deleteMany` 的续写直接失败,不会重试。
|
|
69
65
|
|
|
70
66
|
Preview(`environment: 'preview'`,`NODE_ENV` 不是 production 时的默认值)下,Schema 冲突或 ensure 警告会让请求失败并带上详情;部署环境只记录日志、继续服务。`ignored` 提示从不让请求失败:存储比声明更宽的部分记为 info,`ANNOTATION_NOT_APPLIED`(ensure 不会改写的描述)记为指向 `schema:replace` 的 warning。
|
|
@@ -38,10 +38,10 @@ export function toResponse(error: unknown): Response {
|
|
|
38
38
|
|
|
39
39
|
| code | 含义 |
|
|
40
40
|
| --- | --- |
|
|
41
|
-
| `DATA_NOT_CONFIGURED` (503) | 缺少 TIER0_API_HOST
|
|
41
|
+
| `DATA_NOT_CONFIGURED` (503) | 缺少 TIER0_API_HOST(或 apiHost),或缺少 TIER0_API_KEY(或 apiKey)。平台里两者都由平台注入;没绑 App 的会话两者都没有 |
|
|
42
42
|
| `DATA_TIMEOUT` (503) | UNS 未在超时内应答(每次调用从开始算共 25 秒,含排队与重试,`transportTimeoutMs` 可调);写入时 `outcomeUnknown` 为 true、不重试 |
|
|
43
43
|
| `DATA_OVERLOADED` (503) | 本进程 6 个请求名额一直占满,调用在超时前没排到,**没有发出**(`execution: 'notStarted'`,`retry.safe`);减少并发或合并读取 |
|
|
44
|
-
| `DATA_ABORTED` (499) | 业务请求已中止(`
|
|
44
|
+
| `DATA_ABORTED` (499) | 业务请求已中止(`runWithAbortSignal(request.signal, …)` 传入的信号):排队中的调用不再发出(`notStarted`),已发出的写入 `outcomeUnknown` |
|
|
45
45
|
| `DATA_UNREACHABLE` (503) | 连不上数据服务(读会自动重试);网关地址只在原始错误里,不进 message |
|
|
46
46
|
| `HTTP_502` / `HTTP_503` / `HTTP_504` (503) | 网关在滚动发布、Pod 重启时未经 UNS 直接返回;读会自动重试,写入时 `outcomeUnknown` 为 true、不重试 |
|
|
47
47
|
| `DATA_BAD_RESPONSE` (502) | UNS 应答不符合契约 |
|
|
@@ -58,7 +58,7 @@ export function toResponse(error: unknown): Response {
|
|
|
58
58
|
|
|
59
59
|
| code | 含义与处理 |
|
|
60
60
|
| --- | --- |
|
|
61
|
-
| `SCHEMA_CONFLICT` (409) | 声明无法与存储合并,没有任何改动。`data.conflicts` 列出 `{ alias, path, reason, current?, declared? }`:`TYPE_CONFLICT` → 换新字段名;`LIMIT_EXCEEDED` → 拆分结构;`JOIN_UNSUPPORTED` → 存储中有声明无法放宽的节点,用 `schema:replace` 修复;`
|
|
61
|
+
| `SCHEMA_CONFLICT` (409) | 声明无法与存储合并,没有任何改动。`data.conflicts` 列出 `{ alias, path, reason, current?, declared? }`:`TYPE_CONFLICT` → 换新字段名;`LIMIT_EXCEEDED` → 拆分结构;`JOIN_UNSUPPORTED` → 存储中有声明无法放宽的节点,用 `schema:replace` 修复;`OPEN_OBJECT` → 用 `z.object`;`ALIAS_IN_USE` → 选择未占用的 alias;`NAME_IN_USE` → 选择同目录下不同且有业务含义的 name,保留 alias,不拼随机后缀;`INVALID_INDEX` → 修正索引 |
|
|
62
62
|
| `INVALID_REQUEST` (400),`path=collections/<i>/directoryPath` | 新建集合缺少目录或路径非法。声明实际业务路径后再调用;不回退到缺少业务范围的根级 App 目录,不添加虚构层级 |
|
|
63
63
|
| `SCHEMA_CONFLICT`,`data.reason=DIRECTORY_PATH_CONFLICT` | 路径被已归档目录或同名非目录对象占用;核对真实目录,不能自动恢复、改名或扩大授权 |
|
|
64
64
|
| `DIRECTORY_PATH_NOT_APPLIED`(`ignored` 提示) | 已有 alias 的目录与声明不同;保留当前位置继续服务。需要移动时走明确授权的元数据操作,ensure 不迁移 |
|
|
@@ -80,7 +80,26 @@ export function toResponse(error: unknown): Response {
|
|
|
80
80
|
| `APP_GRANT_REVOKED` (403) | 管理员撤销了本 App 对集合的授权;ensure 不会自动恢复,请管理员处理 |
|
|
81
81
|
| `APP_QUOTA_EXCEEDED` (409) | `data.resource` 指出触达的每 App 上限(64):`collections` → 归档不用的集合;`indexes` → 从声明删索引并 `schema:replace`;`archivedCollections` → 恢复一个或请管理员清理 |
|
|
82
82
|
| `INDEX_QUOTA_EXCEEDED` (409) | Workspace 实际存在的索引已达 200;删除不用的索引 |
|
|
83
|
-
| `COLLECTION_TOO_LARGE` (409) |
|
|
83
|
+
| `COLLECTION_TOO_LARGE` (409) | 仅旧 recordsArchivedAt 墓碑恢复有 2000 条上限(`data.maxRecords`);当前 archive / restore 保留记录,不受该上限限制 |
|
|
84
|
+
| `OWNER_APP_TRASHED`(`data.reason`) | 所属 App 在回收站;恢复 App 后再写入,不触发自动重建 |
|
|
85
|
+
| `DIRECTORY_IN_TRASH`(`data.reason`) | 子项随目录回收,需恢复原目录;不逐个恢复子项 |
|
|
84
86
|
| `FORBIDDEN` (403) | Key 不能做该操作(其他 App 或共享集合),或 Key 已吊销 |
|
|
85
87
|
|
|
86
|
-
记录调用上的 `data.reason`:`COLLECTION_NOT_GRANTED`(403,授权不覆盖该集合或操作)与 `COLLECTION_NOT_FOUND`(403/404
|
|
88
|
+
记录调用上的 `data.reason`:`COLLECTION_NOT_GRANTED`(403,授权不覆盖该集合或操作)与 `COLLECTION_NOT_FOUND`(403/404,集合不存在或写入目标已回收)仅对声明集合触发[自愈](consistency.md#首次访问与自愈),已有 ID 引用直接报告错误;回收不使已有授权的历史读取失效。`APP_PERMISSION_MISSING`(403,`data.missingPermission`)表示 Key 缺少权限,例如删除记录需要 `uns.delete`——业务记录通常以状态结束(`CANCELLED`、`ARCHIVED`)而不是删除。
|
|
89
|
+
|
|
90
|
+
## 平台代理模式的错误
|
|
91
|
+
|
|
92
|
+
只在平台代理模式下出现:`TIER0_RUNTIME` 为 `deployed` / `preview` 而进程里没有 `TIER0_API_KEY`,SDK 不发凭据、请求经节点代理(`sbx-api-proxy`)由它附加 App 的 Key。当前平台给 App 注入 `TIER0_API_KEY`,不会进入这个模式。代理自己产生的错误带响应头 `X-Tier0-Proxy: not-forwarded`,响应体 `{code, message, retry_after_ms?}`,请求**没有到达平台**,所以 `execution` 是 `notStarted`,写入重发也安全。`DataError.code`、`UnsV2Error.code`、`ApiError.errorCode` 都是下表的 `code`。
|
|
93
|
+
|
|
94
|
+
| code | 含义与处理 |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `sbx_proxy_not_ready` (503) | 代理刚启动,还没拿到这台 VM 的绑定。SDK 已按 `retry_after_ms` 重试 3 次,仍失败就稍后再试 |
|
|
97
|
+
| `sbx_proxy_limited` (429) | 超过这台 VM 的上限(64 个并发请求、50 rps、8 个 SSE、16 条 MQTT 等)。SDK 已重试 3 次;减少并发 |
|
|
98
|
+
| `sbx_proxy_credential_pending` (503) | 绑定在,但 App 的 Key 还没送到代理。SDK 已按 `retry_after_ms` 重试 3 次,仍失败就稍后再试 |
|
|
99
|
+
| `sbx_proxy_no_credential` (403) | 这台 VM 永远没有 Key:会话没有绑定 App,只能调 `/openapi/v2/info`。不重试 |
|
|
100
|
+
| `sbx_proxy_unbound` (403) | 这台 VM 当前没有绑定:实例正在启动、停止或被替换,等平台恢复 |
|
|
101
|
+
| `sbx_proxy_bad_request` (400) | 请求格式被拒:absolute-form、`CONNECT`、`Upgrade`、路径含 `.` / `..` / `//` / `%2f` / `%2e` / `%5c` / `;`、请求头超过 32 KiB 或 100 个 |
|
|
102
|
+
| `sbx_proxy_forbidden` (403) | 缺 `X-Tier0-SDK: 1`,或带了 `Forwarded` / `X-Forwarded-*` / `X-Real-IP`。直接用 `fetch` / `curl` 时自己加 `X-Tier0-SDK: 1` |
|
|
103
|
+
| `sbx_proxy_not_found` (404) | 路径不在代理白名单里:只转发 `/openapi/v2/uns/**`、`assets/**`、`notifications/send`、`notifications/get`、`platform/getMembers`、`apps/access-members/list`、`info`(白名单内但 App 的 Key 无权的接口,如 Source / Stream 管理、`assets/upload`,由 Backend 回 403;App 的 Key 在 `assets/` 下只能调 `assets/files/**`) |
|
|
104
|
+
|
|
105
|
+
只有带 `X-Tier0-Proxy: not-forwarded` 的 `503` / `429` 会被 SDK 自动重试;平台返回的 5xx 不重试。
|
|
@@ -150,9 +150,11 @@ export const loginCodes = defineCollection({
|
|
|
150
150
|
|
|
151
151
|
- 字段声明为 `z.number().int()`,存 Unix 毫秒(通常是 `Date.now()`);记录在该时刻再过 `expireAfterSeconds`(60–31536000 秒)后被删除。删除有分钟级延迟,读取时仍要自己比较时间。
|
|
152
152
|
- 每个集合最多 1 个 TTL 索引,只能是单个 `path`、不能 `unique`。缺这个字段的记录不会过期。
|
|
153
|
-
- 过期删除是**物理删除**,不能 restore
|
|
153
|
+
- 过期删除是**物理删除**,不能 restore;集合有效时,软删除的记录同样按 TTL 清理,集合回收后停止 TTL 清理。
|
|
154
154
|
- ensure 只能新增 TTL 或把时间加长;缩短或去掉要通过 tier0 MCP 的 `schema:replace`。
|
|
155
155
|
|
|
156
|
+
集合软删除和 App 回收保留查询索引,物理索引仍占配额;归档不释放索引空间,见[回收规则](../../tier0-sdk-v2-uns/references/delete.md#回收规则)。
|
|
157
|
+
|
|
156
158
|
## 种子数据
|
|
157
159
|
|
|
158
160
|
种子是基线记录:固定 `_id`,各生命周期状态自洽,相互引用用这些 ID;每个集合最多 1000 条。只在 ensure 报告集合为 `created` 时写入一次(每 200 条一个原子分块,分块带固定幂等键,重试安全);修改 `seed` 不会改动已存在的集合。不要在服务启动或列表为空时从代码插入基线数据。构建期可以对每条种子调用 `validateDocument(definition, document)` 校验。
|
|
@@ -91,7 +91,7 @@ export async function ordersScreen(page: number, selectedId: string) {
|
|
|
91
91
|
| --- | --- |
|
|
92
92
|
| `find(filter, { sort, limit, projection })` | 游标,每页最多 200 条;列表读取始终给 `limit` |
|
|
93
93
|
| `findOne(filter)` | 无匹配返回 `null` |
|
|
94
|
-
| `get(ids, { projection })` | 按 `_id` 读取,返回 `{ items, missing }`,`items` 按请求顺序(重复的 ID
|
|
94
|
+
| `get(ids, { projection })` | 按 `_id` 读取,返回 `{ items, missing }`,`items` 按请求顺序(重复的 ID 只取第一次出现);不存在或已删除的 ID 一律进 `missing`。超过 100 个自动分批 |
|
|
95
95
|
| `findPage(filter, { sort, page, pageSize, maxCount })` | 页码分页,一次请求返回 `{ items, total: { count, capped }, page, pageSize }`;pageSize 最多 200,总数默认数到 10000(`maxCount` 最多 100000),超过时 `capped: true` |
|
|
96
96
|
| `countDocuments(filter, { maxCount })` | 返回 `{ count, capped }`,默认数到 100000;只要阈值时给小的 `maxCount`(如角标 `99+`) |
|
|
97
97
|
| `countBy(field, filter)` | 每个取值的数量 `[{ value, count }]`,从多到少;一次 `$group` |
|
|
@@ -161,26 +161,59 @@ export async function bumpAll(items: Collection<{ status: string; views: number
|
|
|
161
161
|
|
|
162
162
|
```ts
|
|
163
163
|
import { z } from 'zod';
|
|
164
|
-
import { createDataClient, defineCollection,
|
|
164
|
+
import { createDataClient, defineCollection, type SubscriptionEvent } from '@tier0/sdk-v2/data';
|
|
165
165
|
|
|
166
166
|
const tickets = defineCollection({ alias: 'tickets', name: '问题工单', directoryPath: ['问题管理'], schema: z.object({ title: z.string(), status: z.enum(['OPEN', 'DONE']) }) });
|
|
167
167
|
const data = createDataClient({ collections: [tickets] });
|
|
168
168
|
|
|
169
|
-
/**
|
|
170
|
-
export async function pumpChanges(
|
|
171
|
-
await
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
});
|
|
169
|
+
/** 服务端自己消费变更(例如刷新缓存)。 */
|
|
170
|
+
export async function pumpChanges(handle: (event: SubscriptionEvent) => void, signal: AbortSignal) {
|
|
171
|
+
for await (const event of data.subscribe({ collections: [tickets], signal })) {
|
|
172
|
+
// reset:续传位置失效或访问范围变化,重新读取当前数据。
|
|
173
|
+
handle(event);
|
|
174
|
+
}
|
|
177
175
|
}
|
|
178
176
|
```
|
|
179
177
|
|
|
180
|
-
- 事件:`{ type: 'change', eventId, collection(alias), collectionId, change: 'record' | 'delete', recordId, revision, occurredAt
|
|
178
|
+
- 事件:`{ type: 'change', eventId, collection(alias), collectionId, change: 'record' | 'delete', recordId, revision, occurredAt }` 或 `{ type: 'reset', reason, message? }`。心跳与重连在 SDK 内部处理;迭代抛出 `DataError` 表示不能再重试(如 403),正常结束只有 `signal` 中止或跳出循环。
|
|
181
179
|
- 事件按写入事务的顺序到达,不是提交顺序,同一记录的 revision 可能乱序。SDK 已丢弃 revision 不大于该记录已投递 revision 的非 `delete` 变更;`delete` 一律投递(迟到的也投递),并清除该记录的 revision(同一 ID 再写入时可能从 1 开始)。收到事件后按 `recordId` 重新读取,不要用事件顺序推断记录的最终状态。
|
|
182
180
|
- `reset`:`CURSOR_EXPIRED`(续传位置已被清理、游标无法解开或租户库已迁移)或 `SCOPE_CHANGED`(访问范围变化,例如集合不再可读)。重新读取当前数据;SDK 随后不带续传位置重新订阅。
|
|
183
|
-
-
|
|
181
|
+
- 订阅以 App 身份运行,看得到 App 被授权集合的全部变更,没有按当前用户的过滤。转发给浏览器时由 App 按访问者角色决定每个浏览器能知道哪些变更(见下一节)。
|
|
184
182
|
- 默认订阅全部已声明的集合(最多 64 个);`recordIds`(最多 100 个)只能配一个集合。
|
|
185
|
-
-
|
|
183
|
+
- 订阅不占数据请求的并发名额,但每个工作区的订阅数有上限(超出时 SDK 按 429 自动退避重连):一个服务端进程只开一条订阅,在进程内扇出,不要每个浏览器连接开一条。App 刚导入还没就绪(`APP_NOT_READY`)时订阅会一直等待,不会结束。
|
|
186
184
|
- 变更保留 24 小时;`lastEventId` 太旧时收到 `reset`。`lastEventId` 必须是收到过的事件 ID(可见 ASCII),否则调用时直接抛 `TypeError`。
|
|
185
|
+
|
|
186
|
+
## 把变更推给浏览器
|
|
187
|
+
|
|
188
|
+
浏览器不直连 UNS 或 MQTT。`createChangeRelay(data)` 在服务端进程里开一条 `data.subscribe`,转成可以挂在 App 路由上的 SSE 响应,`filter` 由 App 按访问者角色决定每个浏览器收到哪些变更:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { z } from 'zod';
|
|
192
|
+
import { createChangeRelay, createDataClient, defineCollection, type RecordChange } from '@tier0/sdk-v2/data';
|
|
193
|
+
import { parseVisitorHeader, VISITOR_HEADER, type Visitor } from '@tier0/sdk-v2/server';
|
|
194
|
+
|
|
195
|
+
const tickets = defineCollection({ alias: 'tickets', name: '问题工单', directoryPath: ['问题管理'], schema: z.object({ title: z.string() }) });
|
|
196
|
+
const data = createDataClient({ collections: [tickets] });
|
|
197
|
+
const relay = createChangeRelay(data); // 每进程一个
|
|
198
|
+
|
|
199
|
+
/** App 自己的规则:哪些角色能看哪个集合。 */
|
|
200
|
+
function canSee(visitor: Visitor, change: RecordChange): boolean {
|
|
201
|
+
return change.collection !== 'tickets' || visitor.appRoles.includes('support');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** GET /api/changes */
|
|
205
|
+
export function changes(request: Request): Response {
|
|
206
|
+
const visitor = parseVisitorHeader(request.headers.get(VISITOR_HEADER));
|
|
207
|
+
return relay.stream({
|
|
208
|
+
filter: (change) => canSee(visitor, change),
|
|
209
|
+
lastEventId: request.headers.get('Last-Event-ID'),
|
|
210
|
+
signal: request.signal,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- 帧:`event: change`(`{ eventId, collection, change, recordId, revision, occurredAt }`,不带记录数据)、`event: reset`(`{ reason }`)、`event: unavailable`(`{ reason }`,上游连不上、仍在重试),每 15 秒一个只带 `id` 的帧推进位置(还没有位置时是注释)。浏览器用 `@tier0/sdk-v2/event-stream` 的 `openEventStream` 读(页面隐藏时断开、可见时带 `Last-Event-ID` 续传,转发器补发或回 `reset`),收到 `change` 后经 App 自己的接口重新读取(那里同样按角色过滤),收到 `reset` 重新加载整个视图。转发器不发 `retry:`,重连节奏由 `openEventStream` 决定。
|
|
216
|
+
- `id` 是转发器自己的位置(`<epoch>.<seq>`),不是 UNS 游标,浏览器拿不到上游位置;只在上游能送达其后所有变更时才给。冷启动时上游第一次连上之前不给位置,连上后先给 `reset`(`upstream-started`)再给位置。上游失败或空闲关闭(最后一个浏览器离开 30 秒)后从最后的 UNS 位置重开,epoch 不变,不漏、不 reset;只有 UNS 回 `reset` 才换 epoch,上游重新连上后给浏览器发 `reset`。重连带 `Last-Event-ID`:在当前 epoch 最近 1000 条里找得到就补发之后的(同样经过 `filter`),找不到就只给这个浏览器一个 `reset`。
|
|
217
|
+
- 上游连不上(超过订阅上限的 429、`APP_NOT_READY` 等)时 `relay.status` 是 `unavailable`,`onStatus(status, error)` 回调,`logger.warn` 记一次,浏览器收到 `event: unavailable`;上游恢复后状态回到 `ready`。
|
|
218
|
+
- `filter` 同步调用,对实时和补发的变更都生效;抛错的变更不发。事件只有标识,但 `recordId` 本身也可能透露信息:规则按记录的,过滤时就把这个浏览器无权知道的记录去掉。
|
|
219
|
+
- 每个浏览器连接 10 分钟(±10%)后结束,浏览器重连时路由重新认证。选项:`collections`、`bufferSize`、`idleCloseMs`、`restartIntervalMs`、`maxRestartIntervalMs`、`heartbeatMs`、`maxStreamMs`、`onStatus`、`logger`。
|
|
@@ -38,4 +38,6 @@ export function replaceBody(expectedRevision: number) {
|
|
|
38
38
|
|
|
39
39
|
Schema 中 `ignored`、`warnings`、`conflicts`、`violations` 的路径用 `.` 连接属性,用 `[]` 表示数组元素(`lines.[].qty`)。
|
|
40
40
|
|
|
41
|
+
集合 archive 的数据与授权变化见[回收规则](../../tier0-sdk-v2-uns/references/delete.md#回收规则),restore 的范围与授权区别见[恢复](../../tier0-sdk-v2-uns/references/restore.md)。
|
|
42
|
+
|
|
41
43
|
新声明的 number/integer 默认带 double/int32 格式;ensure 不改写已有字段注解,原无格式字段仍保持 legacy 显示。已有字段格式迁移需读取并保留完整 Schema,先通过 schema:replace dryRun 检查,再按明确迁移授权执行。
|