forked from eclipse-thingweb/node-wot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinteraction-output.ts
More file actions
175 lines (147 loc) · 6.34 KB
/
interaction-output.ts
File metadata and controls
175 lines (147 loc) · 6.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/********************************************************************************
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
* Document License (2015-05-13) which is available at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/
import * as util from "util";
import * as WoT from "wot-typescript-definitions";
import { ContentSerdes } from "./content-serdes";
import { ProtocolHelpers } from "./core";
import { DataSchemaError, NotReadableError, NotSupportedError } from "./errors";
import { Content } from "./content";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { createLoggers } from "./logger";
const { debug, warn } = createLoggers("core", "interaction-output");
// Problem: strict mode ajv does not accept unknown keywords in schemas
// however property affordances could contain all sort of fields
// since people could use their own ontologies.
// Strict mode has a lot of other checks and it prevents runtime unexpected problems
// TODO: in the future we should use the strict mode
// addUsedSchema may cause memory leak in our use-case / environment (see https://github.com/eclipse-thingweb/node-wot/issues/1062)
const ajv = new Ajv({ strict: false, addUsedSchema: false });
addFormats(ajv);
export class InteractionOutput implements WoT.InteractionOutput {
#content: Content;
#value: unknown;
#valueBuffer?: Buffer<ArrayBuffer>;
#buffer?: ArrayBuffer;
#stream?: ReadableStream;
dataUsed: boolean;
form?: WoT.Form;
schema?: WoT.DataSchema;
public get data(): ReadableStream {
if (this.#stream) {
return this.#stream;
}
if (this.dataUsed) {
throw new Error("Can't read the stream once it has been already used");
}
// Once the stream is created data might be pulled unpredictably
// therefore we assume that it is going to be used to be safe.
this.dataUsed = true;
return (this.#stream = ProtocolHelpers.toWoTStream(this.#content.body) as ReadableStream);
}
constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema) {
this.#content = content;
this.form = form;
this.schema = schema;
this.dataUsed = false;
}
async arrayBuffer(): Promise<ArrayBuffer> {
if (this.#buffer) {
return this.#buffer;
}
if (this.dataUsed) {
throw new Error("Can't read the stream once it has been already used");
}
const data = this.#valueBuffer ?? (await this.#content.toBuffer());
this.dataUsed = true;
const isPooled = data.byteLength < 4096 && data.buffer.byteLength > data.byteLength;
if (isPooled) {
this.#buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
} else {
this.#buffer = data.buffer;
}
//this.#buffer = data as unknown as ArrayBuffer
return this.#buffer;
}
async value<T extends WoT.DataSchemaValue>(): Promise<T> {
// is there any value expected at all?
if (this.schema == null) {
warn(
`No schema defined. Hence undefined is reported for value() function. If you are invoking an action with no output that is on purpose, otherwise consider using arrayBuffer().`
);
return undefined as unknown as T;
}
// the value has been already read?
if (this.#value !== undefined) {
return this.#value as T;
}
if (this.dataUsed) {
throw new NotReadableError("Can't read the stream once it has been already used");
}
if (this.form == null) {
throw new NotReadableError("No form defined");
}
if (
this.schema.const == null &&
this.schema.enum == null &&
this.schema.oneOf == null &&
this.schema.type == null
) {
throw new NotReadableError("No schema type defined");
}
// is content type valid?
if (!ContentSerdes.get().isSupported(this.#content.type)) {
const message = `Content type ${this.#content.type} not supported`;
throw new NotSupportedError(message);
}
// read fully the stream
const bytes = await this.#content.toBuffer();
this.dataUsed = true;
this.#valueBuffer = bytes;
const json = ContentSerdes.get().contentToValue({ type: this.#content.type, body: bytes }, this.schema);
// validate the schema
const validate = ajv.compile<T>(this.schema);
if (!this.ignoreValidation() && !validate(json)) {
debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`);
debug(`value: ${json}`);
debug(`Error: ${validate.errors}`);
throw new DataSchemaError("Invalid value according to DataSchema", json as WoT.DataSchemaValue);
}
this.#value = json;
return json as T;
}
protected ignoreValidation(): boolean {
return false; // by default set to false
}
}
export class ActionInteractionOutput extends InteractionOutput implements WoT.ActionInteractionOutput {
synchronous?: boolean;
constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema, synchronous?: boolean) {
super(content, form, schema);
this.synchronous = synchronous;
}
protected ignoreValidation(): boolean {
return this.synchronous === undefined ? true : !this.synchronous; // validate data for synchronous action only
}
async query<T extends WoT.InteractionOutput>(
params?: WoT.InteractionInput,
options?: WoT.InteractionOptions
): Promise<T> {
throw new Error("Not yet implemented");
}
async cancel(params?: WoT.InteractionInput, options?: WoT.InteractionOptions): Promise<void> {
throw new Error("Not yet implemented");
}
}