mqtt-plus 1.2.0 → 1.3.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dst-stage1/mqtt-plus-auth.js +7 -3
  3. package/dst-stage1/mqtt-plus-base.js +7 -1
  4. package/dst-stage1/mqtt-plus-codec.d.ts +2 -2
  5. package/dst-stage1/mqtt-plus-codec.js +8 -8
  6. package/dst-stage1/mqtt-plus-encode.js +1 -1
  7. package/dst-stage1/mqtt-plus-event.js +14 -13
  8. package/dst-stage1/mqtt-plus-msg.d.ts +21 -6
  9. package/dst-stage1/mqtt-plus-msg.js +86 -36
  10. package/dst-stage1/mqtt-plus-options.d.ts +1 -0
  11. package/dst-stage1/mqtt-plus-options.js +1 -0
  12. package/dst-stage1/mqtt-plus-service.d.ts +0 -2
  13. package/dst-stage1/mqtt-plus-service.js +20 -53
  14. package/dst-stage1/mqtt-plus-sink.d.ts +2 -2
  15. package/dst-stage1/mqtt-plus-sink.js +99 -56
  16. package/dst-stage1/mqtt-plus-source.d.ts +1 -2
  17. package/dst-stage1/mqtt-plus-source.js +91 -56
  18. package/dst-stage1/mqtt-plus-trace.d.ts +2 -2
  19. package/dst-stage1/mqtt-plus-trace.js +5 -8
  20. package/dst-stage1/mqtt-plus-util.d.ts +23 -2
  21. package/dst-stage1/mqtt-plus-util.js +144 -35
  22. package/dst-stage1/mqtt-plus-version.d.ts +4 -0
  23. package/dst-stage1/mqtt-plus-version.js +42 -0
  24. package/dst-stage1/mqtt-plus.d.ts +1 -0
  25. package/dst-stage1/tsc.tsbuildinfo +1 -1
  26. package/dst-stage2/mqtt-plus.cjs.js +440 -241
  27. package/dst-stage2/mqtt-plus.esm.js +440 -241
  28. package/dst-stage2/mqtt-plus.umd.js +11 -11
  29. package/etc/vite.mts +5 -1
  30. package/package.json +1 -1
  31. package/src/mqtt-plus-auth.ts +7 -3
  32. package/src/mqtt-plus-base.ts +8 -2
  33. package/src/mqtt-plus-codec.ts +7 -7
  34. package/src/mqtt-plus-encode.ts +1 -1
  35. package/src/mqtt-plus-event.ts +15 -14
  36. package/src/mqtt-plus-msg.ts +134 -65
  37. package/src/mqtt-plus-options.ts +10 -8
  38. package/src/mqtt-plus-service.ts +26 -60
  39. package/src/mqtt-plus-sink.ts +120 -59
  40. package/src/mqtt-plus-source.ts +106 -62
  41. package/src/mqtt-plus-trace.ts +6 -9
  42. package/src/mqtt-plus-util.ts +166 -38
  43. package/src/mqtt-plus-version.ts +48 -0
  44. package/src/mqtt-plus.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
  ChangeLog
3
3
  =========
4
4
 
5
+ 1.3.0 (2026-02-07)
6
+ ------------------
7
+
8
+ - IMPROVEMENT: add credit-based flow control to sink/source facility
9
+ - IMPROVEMENT: make "buffer" and "stream" fields always mutual-exlusive
10
+ - IMPROVEMENT: provide version field in protocol messages
11
+
12
+ 1.2.1 (2026-02-07)
13
+ ------------------
14
+
15
+ - REFACTOR: use a reference counting subscription class
16
+ - CLEANUP: improve internal validation logic
17
+ - CLEANUP: various code cleanups
18
+
5
19
  1.2.0 (2026-02-06)
6
20
  ------------------
7
21
 
@@ -41,9 +41,9 @@ export class AuthTrait extends MetaTrait {
41
41
  if (credential.length === 0)
42
42
  throw new Error("credential must not be empty");
43
43
  /* use a derived key with minimum length of 32 for JWT HS256 */
44
- const pw = new TextEncoder().encode(credential);
45
- const st = new TextEncoder().encode("mqtt-plus");
46
- this._credential = pbkdf2.deriveKey(sha256.SHA256, pw, st, 100000, 32);
44
+ const pass = new TextEncoder().encode(credential);
45
+ const salt = new TextEncoder().encode("mqtt-plus");
46
+ this._credential = pbkdf2.deriveKey(sha256.SHA256, pass, salt, 600000, 32);
47
47
  }
48
48
  /* issue client-side token on server-side */
49
49
  async issue(payload) {
@@ -93,6 +93,10 @@ export class AuthTrait extends MetaTrait {
93
93
  continue;
94
94
  if (payload.id && payload.id !== clientId)
95
95
  continue;
96
+ if (!Array.isArray(payload.roles))
97
+ continue;
98
+ if (payload.roles.length > 64)
99
+ continue;
96
100
  for (const role of roles) {
97
101
  if (payload.roles.includes(role)) {
98
102
  authenticated = true;
@@ -37,8 +37,14 @@ export class BaseTrait extends TraceTrait {
37
37
  get(_target, prop, _receiver) {
38
38
  if (prop === "isFakeProxy")
39
39
  return true;
40
- else
40
+ else if (typeof prop === "string" && ["on", "off", "once"].includes(prop))
41
41
  return () => { };
42
+ else
43
+ return () => {
44
+ throw new Error(`Underlying MQTT operation "${String(prop)}" called ` +
45
+ "on a null MQTT client -- only MQTT+ \"emit({ ..., dry: true })\" " +
46
+ "is supported in this special operation mode");
47
+ };
42
48
  }
43
49
  });
44
50
  }
@@ -7,10 +7,10 @@ export declare class JSONX {
7
7
  static parse(json: string): unknown;
8
8
  }
9
9
  declare class Codec {
10
- private type;
10
+ private format;
11
11
  private types;
12
12
  private tags;
13
- constructor(type: "cbor" | "json");
13
+ constructor(format: "cbor" | "json");
14
14
  encode(data: unknown): Uint8Array | string;
15
15
  decode(data: Uint8Array | string): unknown;
16
16
  }
@@ -46,8 +46,8 @@ export class JSONX {
46
46
  }
47
47
  /* the encoder/decoder abstraction */
48
48
  class Codec {
49
- constructor(type) {
50
- this.type = type;
49
+ constructor(format) {
50
+ this.format = format;
51
51
  this.types = new CBOR.TypeEncoderMap();
52
52
  this.tags = new Map();
53
53
  /* support direct encoding/decoding of Buffer */
@@ -61,7 +61,7 @@ class Codec {
61
61
  }
62
62
  encode(data) {
63
63
  let result;
64
- if (this.type === "cbor") {
64
+ if (this.format === "cbor") {
65
65
  try {
66
66
  result = CBOR.encode(data, { types: this.types });
67
67
  }
@@ -69,7 +69,7 @@ class Codec {
69
69
  throw new Error("failed to encode CBOR format", { cause: ex });
70
70
  }
71
71
  }
72
- else if (this.type === "json") {
72
+ else if (this.format === "json") {
73
73
  try {
74
74
  result = JSONX.stringify(data);
75
75
  }
@@ -78,12 +78,12 @@ class Codec {
78
78
  }
79
79
  }
80
80
  else
81
- throw new Error(`invalid format "${this.type}"`);
81
+ throw new Error(`invalid format "${this.format}"`);
82
82
  return result;
83
83
  }
84
84
  decode(data) {
85
85
  let result;
86
- if (this.type === "cbor") {
86
+ if (this.format === "cbor") {
87
87
  if (!(data instanceof Uint8Array))
88
88
  throw new Error("failed to decode CBOR format (data type is not Uint8Array)");
89
89
  try {
@@ -93,7 +93,7 @@ class Codec {
93
93
  throw new Error("failed to decode CBOR format", { cause: ex });
94
94
  }
95
95
  }
96
- else if (this.type === "json") {
96
+ else if (this.format === "json") {
97
97
  if (typeof data !== "string")
98
98
  throw new Error("failed to decode JSON format (data type is not string)");
99
99
  try {
@@ -104,7 +104,7 @@ class Codec {
104
104
  }
105
105
  }
106
106
  else
107
- throw new Error(`invalid format "${this.type}"`);
107
+ throw new Error(`invalid format "${this.format}"`);
108
108
  return result;
109
109
  }
110
110
  }
@@ -45,7 +45,7 @@ export class EncodeTrait extends CodecTrait {
45
45
  }
46
46
  buf2arr(data, cons) {
47
47
  let arr;
48
- if (typeof Buffer !== "undefined" && cons === Buffer)
48
+ if (cons === Buffer)
49
49
  arr = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
50
50
  else if (cons === Uint8Array)
51
51
  arr = data;
@@ -25,7 +25,7 @@ import { nanoid } from "nanoid";
25
25
  /* internal requirements */
26
26
  import { EventEmission } from "./mqtt-plus-msg";
27
27
  import { AuthTrait } from "./mqtt-plus-auth";
28
- /* Event Communication Trait */
28
+ /* Event Emission Trait */
29
29
  export class EventTrait extends AuthTrait {
30
30
  constructor() {
31
31
  super(...arguments);
@@ -56,35 +56,36 @@ export class EventTrait extends AuthTrait {
56
56
  if (this.events.has(name))
57
57
  throw new Error(`event: event "${name}" already registered`);
58
58
  /* generate the corresponding MQTT topics for broadcast and direct use */
59
- const topic = share ? `$share/${share}/${name}` : name;
60
- const topicB = this.options.topicMake(topic, "event-emission");
61
- const topicD = this.options.topicMake(topic, "event-emission", this.options.id);
59
+ const topicS = share ? `$share/${share}/${name}` : name;
60
+ const topicB = this.options.topicMake(topicS, "event-emission");
61
+ const topicD = this.options.topicMake(name, "event-emission", this.options.id);
62
+ /* remember the registration */
63
+ this.events.set(name, {
64
+ callback: callback,
65
+ auth
66
+ });
62
67
  /* subscribe to MQTT topics */
63
68
  await Promise.all([
64
69
  this._subscribeTopic(topicB, { qos: 0, ...options }),
65
70
  this._subscribeTopic(topicD, { qos: 0, ...options })
66
71
  ]).catch((err) => {
72
+ this.events.delete(name);
67
73
  this._unsubscribeTopic(topicB).catch(() => { });
68
74
  this._unsubscribeTopic(topicD).catch(() => { });
69
75
  throw err;
70
76
  });
71
- /* remember the registration */
72
- this.events.set(name, {
73
- callback: callback,
74
- auth
75
- });
76
77
  /* provide a registration for subsequent destruction */
77
78
  const registration = {
78
79
  destroy: async () => {
79
80
  if (!this.events.has(name))
80
81
  throw new Error(`destroy: event "${name}" not registered`);
81
- this.events.delete(name);
82
- return Promise.all([
82
+ await Promise.all([
83
83
  this._unsubscribeTopic(topicB),
84
84
  this._unsubscribeTopic(topicD)
85
85
  ]).then(() => { }).catch((err) => {
86
86
  this.error(err, `destroy: failed to unsubscribe from topics for event "${name}"`);
87
87
  });
88
+ this.events.delete(name);
88
89
  }
89
90
  };
90
91
  return registration;
@@ -142,14 +143,14 @@ export class EventTrait extends AuthTrait {
142
143
  if (topicMatch.name !== name)
143
144
  throw new Error(`event name mismatch between topic "${topicMatch.name}" and payload "${name}"`);
144
145
  const handler = this.events.get(name);
146
+ if (handler === undefined)
147
+ throw new Error(`handler for event "${name}" not found`);
145
148
  const params = parsed.params ?? [];
146
149
  const info = { sender: parsed.sender ?? "" };
147
150
  if (parsed.receiver)
148
151
  info.receiver = parsed.receiver;
149
152
  if (parsed.meta)
150
153
  info.meta = parsed.meta;
151
- if (handler === undefined)
152
- throw new Error(`handler for event "${name}" not found`);
153
154
  if (handler.auth)
154
155
  info.authenticated = await this.authenticated(parsed.sender, parsed.auth, handler.auth);
155
156
  Promise.resolve().then(() => {
@@ -1,11 +1,12 @@
1
1
  import { APISchema } from "./mqtt-plus-api";
2
2
  import { EncodeTrait } from "./mqtt-plus-encode";
3
- type MessageType = "event-emission" | "service-call-request" | "service-call-response" | "sink-push-request" | "sink-push-response" | "sink-push-chunk" | "source-fetch-request" | "source-fetch-response" | "source-fetch-chunk";
3
+ type MessageType = "event-emission" | "service-call-request" | "service-call-response" | "sink-push-request" | "sink-push-response" | "sink-push-chunk" | "sink-push-credit" | "source-fetch-request" | "source-fetch-response" | "source-fetch-chunk" | "source-fetch-credit";
4
4
  declare class Base {
5
5
  type: MessageType;
6
6
  id: string;
7
7
  sender?: string | undefined;
8
8
  receiver?: string | undefined;
9
+ version: string;
9
10
  constructor(type: MessageType, id: string, sender?: string | undefined, receiver?: string | undefined);
10
11
  }
11
12
  export declare class EventEmission extends Base {
@@ -39,7 +40,8 @@ export declare class SinkPushResponse extends Base {
39
40
  error?: string | undefined;
40
41
  auth?: string[] | undefined;
41
42
  meta?: Record<string, any> | undefined;
42
- constructor(id: string, name: string, error?: string | undefined, sender?: string, receiver?: string, auth?: string[] | undefined, meta?: Record<string, any> | undefined);
43
+ credit?: number | undefined;
44
+ constructor(id: string, name: string, error?: string | undefined, sender?: string, receiver?: string, auth?: string[] | undefined, meta?: Record<string, any> | undefined, credit?: number | undefined);
43
45
  }
44
46
  export declare class SinkPushChunk extends Base {
45
47
  name: string;
@@ -48,12 +50,18 @@ export declare class SinkPushChunk extends Base {
48
50
  final?: boolean | undefined;
49
51
  constructor(id: string, name: string, chunk?: Uint8Array | undefined, error?: string | undefined, final?: boolean | undefined, sender?: string, receiver?: string);
50
52
  }
53
+ export declare class SinkPushCredit extends Base {
54
+ name: string;
55
+ credit: number;
56
+ constructor(id: string, name: string, credit: number, sender?: string, receiver?: string);
57
+ }
51
58
  export declare class SourceFetchRequest extends Base {
52
59
  name: string;
53
60
  params?: any[] | undefined;
54
61
  auth?: string[] | undefined;
55
62
  meta?: Record<string, any> | undefined;
56
- constructor(id: string, name: string, params?: any[] | undefined, sender?: string, receiver?: string, auth?: string[] | undefined, meta?: Record<string, any> | undefined);
63
+ credit?: number | undefined;
64
+ constructor(id: string, name: string, params?: any[] | undefined, sender?: string, receiver?: string, auth?: string[] | undefined, meta?: Record<string, any> | undefined, credit?: number | undefined);
57
65
  }
58
66
  export declare class SourceFetchResponse extends Base {
59
67
  name: string;
@@ -69,17 +77,24 @@ export declare class SourceFetchChunk extends Base {
69
77
  final?: boolean | undefined;
70
78
  constructor(id: string, name: string, chunk?: Uint8Array | undefined, error?: string | undefined, final?: boolean | undefined, sender?: string, receiver?: string);
71
79
  }
80
+ export declare class SourceFetchCredit extends Base {
81
+ name: string;
82
+ credit: number;
83
+ constructor(id: string, name: string, credit: number, sender?: string, receiver?: string);
84
+ }
72
85
  declare class Msg {
73
86
  makeEventEmission(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): EventEmission;
74
87
  makeServiceCallRequest(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): ServiceCallRequest;
75
88
  makeServiceCallResponse(id: string, result?: any, error?: string, sender?: string, receiver?: string): ServiceCallResponse;
76
89
  makeSinkPushRequest(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): SinkPushRequest;
77
- makeSinkPushResponse(id: string, name: string, error?: string, sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): SinkPushResponse;
90
+ makeSinkPushResponse(id: string, name: string, error?: string, sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>, credit?: number): SinkPushResponse;
78
91
  makeSinkPushChunk(id: string, name: string, chunk?: Uint8Array, error?: string, final?: boolean, sender?: string, receiver?: string): SinkPushChunk;
79
- makeSourceFetchRequest(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): SourceFetchRequest;
92
+ makeSinkPushCredit(id: string, name: string, credit: number, sender?: string, receiver?: string): SinkPushCredit;
93
+ makeSourceFetchRequest(id: string, name: string, params?: any[], sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>, credit?: number): SourceFetchRequest;
80
94
  makeSourceFetchResponse(id: string, name: string, error?: string, sender?: string, receiver?: string, auth?: string[], meta?: Record<string, any>): SourceFetchResponse;
81
95
  makeSourceFetchChunk(id: string, name: string, chunk?: Uint8Array, error?: string, final?: boolean, sender?: string, receiver?: string): SourceFetchChunk;
82
- parse(obj: any): EventEmission | ServiceCallRequest | ServiceCallResponse | SinkPushRequest | SinkPushResponse | SinkPushChunk | SourceFetchRequest | SourceFetchResponse | SourceFetchChunk;
96
+ makeSourceFetchCredit(id: string, name: string, credit: number, sender?: string, receiver?: string): SourceFetchCredit;
97
+ parse(obj: any): EventEmission | ServiceCallRequest | ServiceCallResponse | SinkPushRequest | SinkPushResponse | SinkPushChunk | SinkPushCredit | SourceFetchRequest | SourceFetchResponse | SourceFetchChunk | SourceFetchCredit;
83
98
  }
84
99
  export declare class MsgTrait<T extends APISchema = APISchema> extends EncodeTrait<T> {
85
100
  protected msg: Msg;
@@ -24,10 +24,11 @@
24
24
  /* external requirements */
25
25
  import * as v from "valibot";
26
26
  import { EncodeTrait } from "./mqtt-plus-encode";
27
+ import { version, VERSION, versionToNum } from "./mqtt-plus-version";
27
28
  /* meta validation schema (non-array plain object) */
28
29
  const MetaSchema = v.pipe(v.record(v.string(), v.unknown()), v.check((data) => !Array.isArray(data)));
29
- /* reusable object schema (any non-null object) */
30
- const ObjectSchema = v.custom((input) => typeof input === "object" && input !== null);
30
+ /* reusable auth validation schema (max 8 tokens, max 8192 chars each) */
31
+ const AuthSchema = v.pipe(v.array(v.pipe(v.string(), v.maxLength(8192))), v.maxLength(8));
31
32
  /* base class */
32
33
  class Base {
33
34
  constructor(type, id, sender, receiver) {
@@ -35,9 +36,11 @@ class Base {
35
36
  this.id = id;
36
37
  this.sender = sender;
37
38
  this.receiver = receiver;
39
+ this.version = `MQTT+/${VERSION}`;
38
40
  }
39
41
  }
40
42
  const BaseSchema = {
43
+ version: v.pipe(v.string(), v.regex(/^MQTT\+\/\d+\.\d+$/)),
41
44
  type: v.string(),
42
45
  id: v.string(),
43
46
  sender: v.optional(v.string()),
@@ -57,8 +60,8 @@ const EventEmissionSchema = v.strictObject({
57
60
  ...BaseSchema,
58
61
  type: v.literal("event-emission"),
59
62
  name: v.string(),
60
- params: v.optional(v.array(v.unknown())),
61
- auth: v.optional(v.array(v.string())),
63
+ params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
64
+ auth: v.optional(AuthSchema),
62
65
  meta: v.optional(MetaSchema)
63
66
  });
64
67
  /* service request */
@@ -75,8 +78,8 @@ const ServiceCallRequestSchema = v.strictObject({
75
78
  ...BaseSchema,
76
79
  type: v.literal("service-call-request"),
77
80
  name: v.string(),
78
- params: v.optional(v.array(v.unknown())),
79
- auth: v.optional(v.array(v.string())),
81
+ params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
82
+ auth: v.optional(AuthSchema),
80
83
  meta: v.optional(MetaSchema)
81
84
  });
82
85
  /* service response */
@@ -107,18 +110,19 @@ const SinkPushRequestSchema = v.strictObject({
107
110
  ...BaseSchema,
108
111
  type: v.literal("sink-push-request"),
109
112
  name: v.string(),
110
- params: v.optional(v.array(v.unknown())),
111
- auth: v.optional(v.array(v.string())),
113
+ params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
114
+ auth: v.optional(AuthSchema),
112
115
  meta: v.optional(MetaSchema)
113
116
  });
114
117
  /* sink push response (ack/nak) */
115
118
  export class SinkPushResponse extends Base {
116
- constructor(id, name, error, sender, receiver, auth, meta) {
119
+ constructor(id, name, error, sender, receiver, auth, meta, credit) {
117
120
  super("sink-push-response", id, sender, receiver);
118
121
  this.name = name;
119
122
  this.error = error;
120
123
  this.auth = auth;
121
124
  this.meta = meta;
125
+ this.credit = credit;
122
126
  }
123
127
  }
124
128
  const SinkPushResponseSchema = v.strictObject({
@@ -126,8 +130,9 @@ const SinkPushResponseSchema = v.strictObject({
126
130
  type: v.literal("sink-push-response"),
127
131
  name: v.string(),
128
132
  error: v.optional(v.string()),
129
- auth: v.optional(v.array(v.string())),
130
- meta: v.optional(MetaSchema)
133
+ auth: v.optional(AuthSchema),
134
+ meta: v.optional(MetaSchema),
135
+ credit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)))
131
136
  });
132
137
  /* sink push chunk (actual data transfer) */
133
138
  export class SinkPushChunk extends Base {
@@ -143,27 +148,43 @@ const SinkPushChunkSchema = v.strictObject({
143
148
  ...BaseSchema,
144
149
  type: v.literal("sink-push-chunk"),
145
150
  name: v.string(),
146
- chunk: v.optional(ObjectSchema),
151
+ chunk: v.optional(v.instance(Uint8Array)),
147
152
  error: v.optional(v.string()),
148
153
  final: v.optional(v.boolean())
149
154
  });
155
+ /* sink push credit (credit replenishment for push flow control) */
156
+ export class SinkPushCredit extends Base {
157
+ constructor(id, name, credit, sender, receiver) {
158
+ super("sink-push-credit", id, sender, receiver);
159
+ this.name = name;
160
+ this.credit = credit;
161
+ }
162
+ }
163
+ const SinkPushCreditSchema = v.strictObject({
164
+ ...BaseSchema,
165
+ type: v.literal("sink-push-credit"),
166
+ name: v.string(),
167
+ credit: v.pipe(v.number(), v.integer(), v.minValue(1))
168
+ });
150
169
  /* source fetch request */
151
170
  export class SourceFetchRequest extends Base {
152
- constructor(id, name, params, sender, receiver, auth, meta) {
171
+ constructor(id, name, params, sender, receiver, auth, meta, credit) {
153
172
  super("source-fetch-request", id, sender, receiver);
154
173
  this.name = name;
155
174
  this.params = params;
156
175
  this.auth = auth;
157
176
  this.meta = meta;
177
+ this.credit = credit;
158
178
  }
159
179
  }
160
180
  const SourceFetchRequestSchema = v.strictObject({
161
181
  ...BaseSchema,
162
182
  type: v.literal("source-fetch-request"),
163
183
  name: v.string(),
164
- params: v.optional(v.array(v.unknown())),
165
- auth: v.optional(v.array(v.string())),
166
- meta: v.optional(MetaSchema)
184
+ params: v.optional(v.pipe(v.array(v.unknown()), v.maxLength(64))),
185
+ auth: v.optional(AuthSchema),
186
+ meta: v.optional(MetaSchema),
187
+ credit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)))
167
188
  });
168
189
  /* source fetch response (ack/nak) */
169
190
  export class SourceFetchResponse extends Base {
@@ -180,7 +201,7 @@ const SourceFetchResponseSchema = v.strictObject({
180
201
  type: v.literal("source-fetch-response"),
181
202
  name: v.string(),
182
203
  error: v.optional(v.string()),
183
- auth: v.optional(v.array(v.string())),
204
+ auth: v.optional(AuthSchema),
184
205
  meta: v.optional(MetaSchema)
185
206
  });
186
207
  /* source fetch chunk (actual data transfer) */
@@ -197,55 +218,73 @@ const SourceFetchChunkSchema = v.strictObject({
197
218
  ...BaseSchema,
198
219
  type: v.literal("source-fetch-chunk"),
199
220
  name: v.string(),
200
- chunk: v.optional(ObjectSchema),
221
+ chunk: v.optional(v.instance(Uint8Array)),
201
222
  error: v.optional(v.string()),
202
223
  final: v.optional(v.boolean())
203
224
  });
225
+ /* source fetch credit (credit replenishment for fetch flow control) */
226
+ export class SourceFetchCredit extends Base {
227
+ constructor(id, name, credit, sender, receiver) {
228
+ super("source-fetch-credit", id, sender, receiver);
229
+ this.name = name;
230
+ this.credit = credit;
231
+ }
232
+ }
233
+ const SourceFetchCreditSchema = v.strictObject({
234
+ ...BaseSchema,
235
+ type: v.literal("source-fetch-credit"),
236
+ name: v.string(),
237
+ credit: v.pipe(v.number(), v.integer(), v.minValue(1))
238
+ });
204
239
  /* utility class */
205
240
  class Msg {
206
- /* factory for event emission */
241
+ /* factories for creating objects */
207
242
  makeEventEmission(id, name, params, sender, receiver, auth, meta) {
208
243
  return new EventEmission(id, name, params, sender, receiver, auth, meta);
209
244
  }
210
- /* factory for service request */
211
245
  makeServiceCallRequest(id, name, params, sender, receiver, auth, meta) {
212
246
  return new ServiceCallRequest(id, name, params, sender, receiver, auth, meta);
213
247
  }
214
- /* factory for service response */
215
248
  makeServiceCallResponse(id, result, error, sender, receiver) {
216
249
  return new ServiceCallResponse(id, result, error, sender, receiver);
217
250
  }
218
- /* factory for sink push request */
219
251
  makeSinkPushRequest(id, name, params, sender, receiver, auth, meta) {
220
252
  return new SinkPushRequest(id, name, params, sender, receiver, auth, meta);
221
253
  }
222
- /* factory for sink push response */
223
- makeSinkPushResponse(id, name, error, sender, receiver, auth, meta) {
224
- return new SinkPushResponse(id, name, error, sender, receiver, auth, meta);
254
+ makeSinkPushResponse(id, name, error, sender, receiver, auth, meta, credit) {
255
+ return new SinkPushResponse(id, name, error, sender, receiver, auth, meta, credit);
225
256
  }
226
- /* factory for sink push chunk */
227
257
  makeSinkPushChunk(id, name, chunk, error, final, sender, receiver) {
228
258
  return new SinkPushChunk(id, name, chunk, error, final, sender, receiver);
229
259
  }
230
- /* factory for source fetch request */
231
- makeSourceFetchRequest(id, name, params, sender, receiver, auth, meta) {
232
- return new SourceFetchRequest(id, name, params, sender, receiver, auth, meta);
260
+ makeSinkPushCredit(id, name, credit, sender, receiver) {
261
+ return new SinkPushCredit(id, name, credit, sender, receiver);
262
+ }
263
+ makeSourceFetchRequest(id, name, params, sender, receiver, auth, meta, credit) {
264
+ return new SourceFetchRequest(id, name, params, sender, receiver, auth, meta, credit);
233
265
  }
234
- /* factory for source fetch response */
235
266
  makeSourceFetchResponse(id, name, error, sender, receiver, auth, meta) {
236
267
  return new SourceFetchResponse(id, name, error, sender, receiver, auth, meta);
237
268
  }
238
- /* factory for source fetch chunk */
239
269
  makeSourceFetchChunk(id, name, chunk, error, final, sender, receiver) {
240
270
  return new SourceFetchChunk(id, name, chunk, error, final, sender, receiver);
241
271
  }
272
+ makeSourceFetchCredit(id, name, credit, sender, receiver) {
273
+ return new SourceFetchCredit(id, name, credit, sender, receiver);
274
+ }
242
275
  /* parse any object into typed object */
243
276
  parse(obj) {
277
+ /* sanity check input */
244
278
  if (typeof obj !== "object" || obj === null)
245
279
  throw new Error("invalid argument: not an object");
246
- if (typeof obj.type !== "string")
247
- throw new Error("invalid object: missing or invalid \"type\" field");
248
- /* dispatch according to type indication by field */
280
+ /* sanity check version */
281
+ if (typeof obj.version !== "string")
282
+ throw new Error("invalid object: missing or invalid \"version\" field");
283
+ const m = obj.version.match(/^MQTT\+\/(\d+\.\d+)$/);
284
+ const V = m !== null ? versionToNum(m[1]) : 0.0;
285
+ if (V !== version)
286
+ throw new Error(`protocol version mismatch (expected version ${VERSION}, got version ${obj.version})`);
287
+ /* helper function for Valibot-based validation */
249
288
  const parseObject = (obj, name, schema) => {
250
289
  const res = v.safeParse(schema, obj);
251
290
  if (!res.success) {
@@ -254,6 +293,9 @@ class Msg {
254
293
  }
255
294
  return res.output;
256
295
  };
296
+ /* dispatch according to type indication by field */
297
+ if (typeof obj.type !== "string")
298
+ throw new Error("invalid object: missing or invalid \"type\" field");
257
299
  if (obj.type === "event-emission") {
258
300
  const out = parseObject(obj, "EventEmission", EventEmissionSchema);
259
301
  return this.makeEventEmission(out.id, out.name, out.params, out.sender, out.receiver, out.auth, out.meta);
@@ -272,15 +314,19 @@ class Msg {
272
314
  }
273
315
  else if (obj.type === "sink-push-response") {
274
316
  const out = parseObject(obj, "SinkPushResponse", SinkPushResponseSchema);
275
- return this.makeSinkPushResponse(out.id, out.name, out.error, out.sender, out.receiver, out.auth, out.meta);
317
+ return this.makeSinkPushResponse(out.id, out.name, out.error, out.sender, out.receiver, out.auth, out.meta, out.credit);
276
318
  }
277
319
  else if (obj.type === "sink-push-chunk") {
278
320
  const out = parseObject(obj, "SinkPushChunk", SinkPushChunkSchema);
279
321
  return this.makeSinkPushChunk(out.id, out.name, out.chunk, out.error, out.final, out.sender, out.receiver);
280
322
  }
323
+ else if (obj.type === "sink-push-credit") {
324
+ const out = parseObject(obj, "SinkPushCredit", SinkPushCreditSchema);
325
+ return this.makeSinkPushCredit(out.id, out.name, out.credit, out.sender, out.receiver);
326
+ }
281
327
  else if (obj.type === "source-fetch-request") {
282
328
  const out = parseObject(obj, "SourceFetchRequest", SourceFetchRequestSchema);
283
- return this.makeSourceFetchRequest(out.id, out.name, out.params, out.sender, out.receiver, out.auth, out.meta);
329
+ return this.makeSourceFetchRequest(out.id, out.name, out.params, out.sender, out.receiver, out.auth, out.meta, out.credit);
284
330
  }
285
331
  else if (obj.type === "source-fetch-response") {
286
332
  const out = parseObject(obj, "SourceFetchResponse", SourceFetchResponseSchema);
@@ -290,6 +336,10 @@ class Msg {
290
336
  const out = parseObject(obj, "SourceFetchChunk", SourceFetchChunkSchema);
291
337
  return this.makeSourceFetchChunk(out.id, out.name, out.chunk, out.error, out.final, out.sender, out.receiver);
292
338
  }
339
+ else if (obj.type === "source-fetch-credit") {
340
+ const out = parseObject(obj, "SourceFetchCredit", SourceFetchCreditSchema);
341
+ return this.makeSourceFetchCredit(out.id, out.name, out.credit, out.sender, out.receiver);
342
+ }
293
343
  else
294
344
  throw new Error("invalid object: not of any known type");
295
345
  }
@@ -11,6 +11,7 @@ export interface APIOptions {
11
11
  codec: "cbor" | "json";
12
12
  timeout: number;
13
13
  chunkSize: number;
14
+ chunkCredit: number;
14
15
  topicMake: TopicMake;
15
16
  topicMatch: TopicMatch;
16
17
  }
@@ -33,6 +33,7 @@ export class OptionsTrait {
33
33
  codec: "cbor",
34
34
  timeout: 10 * 1000,
35
35
  chunkSize: 16 * 1024,
36
+ chunkCredit: 4,
36
37
  topicMake: (name, operation, peerId) => {
37
38
  return `${name}/${operation}/${peerId ?? "any"}`;
38
39
  },
@@ -23,7 +23,5 @@ export declare class ServiceTrait<T extends APISchema = APISchema> extends Event
23
23
  options?: IClientPublishOptions;
24
24
  meta?: Record<string, any>;
25
25
  }): Promise<ReturnType<T[K]>>;
26
- private callSubscribe;
27
- private callUnsubscribe;
28
26
  protected _dispatchMessage(topic: string, parsed: any): Promise<void>;
29
27
  }