tigerbeetle-node 0.14.157 → 0.14.159

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/src/node.zig CHANGED
@@ -1,9 +1,13 @@
1
1
  const std = @import("std");
2
2
  const assert = std.debug.assert;
3
+ const allocator = std.heap.c_allocator;
3
4
 
4
5
  const c = @import("c.zig");
5
6
  const translate = @import("translate.zig");
6
- const tb = @import("../../../tigerbeetle.zig");
7
+ const tb = struct {
8
+ pub usingnamespace @import("../../../tigerbeetle.zig");
9
+ pub usingnamespace @import("../../c/tb_client.zig");
10
+ };
7
11
 
8
12
  const Account = tb.Account;
9
13
  const AccountFlags = tb.AccountFlags;
@@ -15,783 +19,472 @@ const CreateTransfersResult = tb.CreateTransfersResult;
15
19
  const Storage = @import("../../../storage.zig").Storage;
16
20
  const StateMachine = @import("../../../state_machine.zig").StateMachineType(Storage, constants.state_machine_config);
17
21
  const Operation = StateMachine.Operation;
18
- const MessageBus = @import("../../../message_bus.zig").MessageBusClient;
19
- const MessagePool = @import("../../../message_pool.zig").MessagePool;
20
- const IO = @import("../../../io.zig").IO;
21
22
  const constants = @import("../../../constants.zig");
22
-
23
23
  const vsr = @import("../../../vsr.zig");
24
- const Header = vsr.Header;
25
- const Client = vsr.Client(StateMachine, MessageBus);
26
24
 
27
25
  pub const std_options = struct {
28
26
  // Since this is running in application space, log only critical messages to reduce noise.
29
27
  pub const log_level: std.log.Level = .err;
30
28
  };
31
29
 
30
+ // Cached value for JS (null).
31
+ var napi_null: c.napi_value = undefined;
32
+
32
33
  /// N-API will call this constructor automatically to register the module.
33
34
  export fn napi_register_module_v1(env: c.napi_env, exports: c.napi_value) c.napi_value {
35
+ napi_null = translate.capture_null(env) catch return null;
36
+
34
37
  translate.register_function(env, exports, "init", init) catch return null;
35
38
  translate.register_function(env, exports, "deinit", deinit) catch return null;
36
- translate.register_function(env, exports, "request", request) catch return null;
37
- translate.register_function(env, exports, "raw_request", raw_request) catch return null;
38
- translate.register_function(env, exports, "tick", tick) catch return null;
39
+ translate.register_function(env, exports, "submit", submit) catch return null;
40
+ return exports;
41
+ }
39
42
 
40
- translate.u32_into_object(
43
+ // Add-on code
44
+
45
+ fn init(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
46
+ const args = translate.extract_args(env, info, .{
47
+ .count = 1,
48
+ .function = "init",
49
+ }) catch return null;
50
+
51
+ const cluster = translate.u32_from_object(env, args[0], "cluster_id") catch return null;
52
+ const concurrency = translate.u32_from_object(env, args[0], "concurrency") catch return null;
53
+ const addresses = translate.slice_from_object(
41
54
  env,
42
- exports,
43
- "tick_ms",
44
- constants.tick_ms,
45
- "failed to add tick_ms to exports",
55
+ args[0],
56
+ "replica_addresses",
46
57
  ) catch return null;
47
58
 
48
- const allocator = std.heap.c_allocator;
49
- var global = Globals.init(allocator, env) catch {
50
- std.log.err("Failed to initialise environment.\n", .{});
51
- return null;
52
- };
53
- errdefer global.deinit();
54
-
55
- // Tie the global state to this Node.js environment. This allows us to be thread safe.
56
- // See https://nodejs.org/api/n-api.html#n_api_environment_life_cycle_apis.
57
- // A cleanup function is registered as well that Node will call when the environment
58
- // is torn down. Be careful not to call this function again as it will overwrite the global
59
- // state.
60
- translate.set_instance_data(
61
- env,
62
- @ptrCast(@alignCast(global)),
63
- Globals.destroy,
64
- ) catch {
65
- global.deinit();
66
- return null;
67
- };
59
+ return create(env, cluster, concurrency, addresses) catch null;
60
+ }
68
61
 
69
- return exports;
62
+ fn deinit(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
63
+ const args = translate.extract_args(env, info, .{
64
+ .count = 1,
65
+ .function = "deinit",
66
+ }) catch return null;
67
+
68
+ destroy(env, args[0]) catch {};
69
+ return null;
70
70
  }
71
71
 
72
- const Globals = struct {
73
- allocator: std.mem.Allocator,
74
- io: IO,
75
- napi_undefined: c.napi_value,
76
-
77
- pub fn init(allocator: std.mem.Allocator, env: c.napi_env) !*Globals {
78
- const self = try allocator.create(Globals);
79
- errdefer allocator.destroy(self);
80
-
81
- self.allocator = allocator;
82
-
83
- // Be careful to size the SQ ring to only a few SQE entries and to share a single IO
84
- // instance across multiple clients to stay under kernel limits:
85
- //
86
- // The memory required by io_uring is accounted under the rlimit memlocked option, which can
87
- // be quite low on some setups (64K). The default is usually enough for most use cases, but
88
- // bigger rings or things like registered buffers deplete it quickly. Root isn't under this
89
- // restriction, but regular users are.
90
- //
91
- // Check `/etc/security/limits.conf` for user settings, or `/etc/systemd/user.conf` and
92
- // `/etc/systemd/system.conf` for systemd setups.
93
- self.io = IO.init(32, 0) catch {
94
- return translate.throw(env, "Failed to initialize io_uring");
95
- };
96
- errdefer self.io.deinit();
97
-
98
- if (c.napi_get_undefined(env, &self.napi_undefined) != c.napi_ok) {
99
- return translate.throw(env, "Failed to capture the value of \"undefined\".");
100
- }
72
+ fn submit(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
73
+ const args = translate.extract_args(env, info, .{
74
+ .count = 4,
75
+ .function = "submit",
76
+ }) catch return null;
101
77
 
102
- return self;
78
+ const operation_int = translate.u32_from_value(env, args[1], "operation") catch return null;
79
+ if (!@as(vsr.Operation, @enumFromInt(operation_int)).valid(StateMachine)) {
80
+ translate.throw(env, "Unknown operation.") catch return null;
103
81
  }
104
82
 
105
- pub fn deinit(self: *Globals) void {
106
- self.io.deinit();
107
- self.allocator.destroy(self);
83
+ var is_array: bool = undefined;
84
+ if (c.napi_is_array(env, args[2], &is_array) != c.napi_ok) {
85
+ translate.throw(env, "Failed to check array argument type.") catch return null;
86
+ }
87
+ if (!is_array) {
88
+ translate.throw(env, "Array argument must be an [object Array].") catch return null;
108
89
  }
109
90
 
110
- pub fn destroy(env: c.napi_env, data: ?*anyopaque, hint: ?*anyopaque) callconv(.C) void {
111
- _ = env;
112
- _ = hint;
113
-
114
- const self = globalsCast(data.?);
115
- self.deinit();
91
+ var callback_type: c.napi_valuetype = undefined;
92
+ if (c.napi_typeof(env, args[3], &callback_type) != c.napi_ok) {
93
+ translate.throw(env, "Failed to check callback argument type.") catch return null;
94
+ }
95
+ if (callback_type != c.napi_function) {
96
+ translate.throw(env, "Callback argument must be a Function.") catch return null;
116
97
  }
117
- };
118
98
 
119
- fn globalsCast(globals_raw: *anyopaque) *Globals {
120
- return @ptrCast(@alignCast(globals_raw));
99
+ request(
100
+ env,
101
+ args[0], // tb_client
102
+ @enumFromInt(@as(u8, @intCast(operation_int))),
103
+ args[2], // request array
104
+ args[3], // callback
105
+ ) catch {};
106
+ return null;
121
107
  }
122
108
 
123
- const Context = struct {
124
- io: *IO,
125
- addresses: []std.net.Address,
126
- client: Client,
127
- message_pool: MessagePool,
128
-
129
- fn create(
130
- env: c.napi_env,
131
- allocator: std.mem.Allocator,
132
- io: *IO,
133
- cluster: u32,
134
- addresses_raw: []const u8,
135
- ) !c.napi_value {
136
- const context = try allocator.create(Context);
137
- errdefer allocator.destroy(context);
138
-
139
- context.io = io;
140
- context.message_pool = try MessagePool.init(allocator, .client);
141
- errdefer context.message_pool.deinit(allocator);
142
-
143
- context.addresses = try vsr.parse_addresses(allocator, addresses_raw, constants.replicas_max);
144
- errdefer allocator.free(context.addresses);
145
- assert(context.addresses.len > 0);
146
-
147
- const client_id = std.crypto.random.int(u128);
148
- context.client = try Client.init(
149
- allocator,
150
- client_id,
151
- cluster,
152
- @as(u8, @intCast(context.addresses.len)),
153
- &context.message_pool,
154
- .{
155
- .configuration = context.addresses,
156
- .io = context.io,
157
- },
158
- );
159
- errdefer context.client.deinit(allocator);
109
+ // tb_client Logic
160
110
 
161
- return try translate.create_external(env, context);
111
+ fn create(env: c.napi_env, cluster_id: u32, concurrency: u32, addresses: []const u8) !c.napi_value {
112
+ var tsfn_name: c.napi_value = undefined;
113
+ if (c.napi_create_string_utf8(env, "tb_client", c.NAPI_AUTO_LENGTH, &tsfn_name) != c.napi_ok) {
114
+ return translate.throw(env, "Failed to create resource name for thread-safe function.");
162
115
  }
163
- };
164
116
 
165
- fn contextCast(context_raw: *anyopaque) !*Context {
166
- return @ptrCast(@alignCast(context_raw));
167
- }
168
-
169
- fn validate_timestamp(env: c.napi_env, object: c.napi_value) !u64 {
170
- const timestamp = try translate.u64_from_object(env, object, "timestamp");
171
- if (timestamp != 0) {
172
- return translate.throw(
173
- env,
174
- "Timestamp should be set as 0 as this will be set correctly by the Server.",
175
- );
117
+ var completion_tsfn: c.napi_threadsafe_function = undefined;
118
+ if (c.napi_create_threadsafe_function(
119
+ env,
120
+ null, // No javascript function to call directly from here.
121
+ null, // No async resource.
122
+ tsfn_name,
123
+ 0, // Max queue size of 0 means no limit.
124
+ 1, // Number of acquires/threads that will be calling this TSFN.
125
+ null, // No finalization data.
126
+ null, // No finalization callback.
127
+ null, // No custom context.
128
+ on_completion_js, // Function to call on JS thread when TSFN is called.
129
+ &completion_tsfn, // TSFN out handle.
130
+ ) != c.napi_ok) {
131
+ return translate.throw(env, "Failed to create thread-safe function.");
176
132
  }
133
+ errdefer if (c.napi_release_threadsafe_function(
134
+ completion_tsfn,
135
+ c.napi_tsfn_abort,
136
+ ) != c.napi_ok) {
137
+ std.log.warn("Failed to release allocated thread-safe function on error.", .{});
138
+ };
177
139
 
178
- return timestamp;
179
- }
140
+ if (c.napi_acquire_threadsafe_function(completion_tsfn) != c.napi_ok) {
141
+ return translate.throw(env, "Failed to acquire reference to thread-safe function.");
142
+ }
180
143
 
181
- fn decode_from_object(comptime T: type, env: c.napi_env, object: c.napi_value) !T {
182
- return switch (T) {
183
- Transfer => Transfer{
184
- .id = try translate.u128_from_object(env, object, "id"),
185
- .debit_account_id = try translate.u128_from_object(env, object, "debit_account_id"),
186
- .credit_account_id = try translate.u128_from_object(env, object, "credit_account_id"),
187
- .amount = try translate.u128_from_object(env, object, "amount"),
188
- .pending_id = try translate.u128_from_object(env, object, "pending_id"),
189
- .user_data_128 = try translate.u128_from_object(env, object, "user_data_128"),
190
- .user_data_64 = try translate.u64_from_object(env, object, "user_data_64"),
191
- .user_data_32 = try translate.u32_from_object(env, object, "user_data_32"),
192
- .timeout = try translate.u32_from_object(env, object, "timeout"),
193
- .ledger = try translate.u32_from_object(env, object, "ledger"),
194
- .code = try translate.u16_from_object(env, object, "code"),
195
- .flags = @as(TransferFlags, @bitCast(try translate.u16_from_object(env, object, "flags"))),
196
- .timestamp = try validate_timestamp(env, object),
197
- },
198
- Account => Account{
199
- .id = try translate.u128_from_object(env, object, "id"),
200
- .debits_pending = try translate.u128_from_object(env, object, "debits_pending"),
201
- .debits_posted = try translate.u128_from_object(env, object, "debits_posted"),
202
- .credits_pending = try translate.u128_from_object(env, object, "credits_pending"),
203
- .credits_posted = try translate.u128_from_object(env, object, "credits_posted"),
204
- .user_data_128 = try translate.u128_from_object(env, object, "user_data_128"),
205
- .user_data_64 = try translate.u64_from_object(env, object, "user_data_64"),
206
- .user_data_32 = try translate.u32_from_object(env, object, "user_data_32"),
207
- .reserved = try translate.u32_from_object(env, object, "reserved"),
208
- .ledger = try translate.u32_from_object(env, object, "ledger"),
209
- .code = try translate.u16_from_object(env, object, "code"),
210
- .flags = @bitCast(try translate.u16_from_object(env, object, "flags")),
211
- .timestamp = try validate_timestamp(env, object),
212
- },
213
- u128 => try translate.u128_from_value(env, object, "lookup"),
214
- else => unreachable,
144
+ const client = tb.init(
145
+ allocator,
146
+ cluster_id,
147
+ addresses,
148
+ concurrency,
149
+ @intFromPtr(completion_tsfn),
150
+ on_completion,
151
+ ) catch |err| switch (err) {
152
+ error.OutOfMemory => return translate.throw(env, "Failed to allocate memory for Client."),
153
+ error.Unexpected => return translate.throw(env, "Unexpected error occured on Client."),
154
+ error.AddressInvalid => return translate.throw(env, "Invalid replica address."),
155
+ error.AddressLimitExceeded => return translate.throw(env, "Too many replica addresses."),
156
+ error.ConcurrencyMaxInvalid => return translate.throw(env, "Concurrency is too high."),
157
+ error.SystemResources => return translate.throw(env, "Failed to reserve system resources."),
158
+ error.NetworkSubsystemFailed => return translate.throw(env, "Network stack failure."),
215
159
  };
216
- }
160
+ errdefer tb.deinit(client);
217
161
 
218
- pub fn decode_events(
219
- env: c.napi_env,
220
- array: c.napi_value,
221
- operation: Operation,
222
- output: []u8,
223
- ) !usize {
224
- return switch (operation) {
225
- .create_accounts => try decode_events_from_array(env, array, Account, output),
226
- .create_transfers => try decode_events_from_array(env, array, Transfer, output),
227
- .lookup_accounts => try decode_events_from_array(env, array, u128, output),
228
- .lookup_transfers => try decode_events_from_array(env, array, u128, output),
229
- };
162
+ return try translate.create_external(env, client);
230
163
  }
231
164
 
232
- fn decode_events_from_array(
233
- env: c.napi_env,
234
- array: c.napi_value,
235
- comptime T: type,
236
- output: []u8,
237
- ) !usize {
238
- const array_length = try translate.array_length(env, array);
239
- if (array_length < 1) return translate.throw(env, "Batch must contain at least one event.");
240
-
241
- const body_length = @sizeOf(T) * array_length;
242
- if (@sizeOf(Header) + body_length > constants.message_size_max) {
243
- return translate.throw(env, "Batch is larger than the maximum message size.");
244
- }
165
+ fn destroy(env: c.napi_env, context: c.napi_value) !void {
166
+ const client_ptr = try translate.value_external(
167
+ env,
168
+ context,
169
+ "Failed to get client context pointer.",
170
+ );
171
+ const client: tb.tb_client_t = @ptrCast(@alignCast(client_ptr.?));
172
+ defer tb.deinit(client);
245
173
 
246
- // We take a slice on `output` to ensure that its length is a multiple of @sizeOf(T) to prevent
247
- // a safety-checked runtime panic from `bytesAsSlice` for non-multiple sizes.
248
- var results = std.mem.bytesAsSlice(T, output[0..body_length]);
174
+ const completion_ctx = tb.completion_context(client);
175
+ const completion_tsfn: c.napi_threadsafe_function = @ptrFromInt(completion_ctx);
249
176
 
250
- var i: u32 = 0;
251
- while (i < array_length) : (i += 1) {
252
- const entry = try translate.array_element(env, array, i);
253
- results[i] = try decode_from_object(T, env, entry);
177
+ if (c.napi_release_threadsafe_function(completion_tsfn, c.napi_tsfn_abort) != c.napi_ok) {
178
+ return translate.throw(env, "Failed to release allocated thread-safe function on error.");
254
179
  }
255
-
256
- return body_length;
257
180
  }
258
181
 
259
- fn encode_napi_results_array(
260
- comptime Result: type,
182
+ fn request(
261
183
  env: c.napi_env,
262
- data: []const u8,
263
- ) !c.napi_value {
264
- const results = std.mem.bytesAsSlice(Result, data);
265
- const napi_array = try translate.create_array(
184
+ context: c.napi_value,
185
+ operation: Operation,
186
+ array: c.napi_value,
187
+ callback: c.napi_value,
188
+ ) !void {
189
+ const client_ptr = try translate.value_external(
266
190
  env,
267
- @as(u32, @intCast(results.len)),
268
- "Failed to allocate array for results.",
191
+ context,
192
+ "Failed to get client context pointer.",
269
193
  );
194
+ const client: tb.tb_client_t = @ptrCast(@alignCast(client_ptr.?));
195
+
196
+ const packet = blk: {
197
+ var packet_ptr: ?*tb.tb_packet_t = undefined;
198
+ switch (tb.acquire_packet(client, &packet_ptr)) {
199
+ .ok => break :blk packet_ptr.?,
200
+ .shutdown => return translate.throw(env, "Client was shutdown."),
201
+ .concurrency_max_exceeded => return translate.throw(env, "Too many concurrent requests."),
202
+ }
203
+ };
204
+ errdefer tb.release_packet(client, packet);
270
205
 
271
- switch (Result) {
272
- CreateAccountsResult, CreateTransfersResult => {
273
- var i: u32 = 0;
274
- while (i < results.len) : (i += 1) {
275
- const result = results[i];
276
- const napi_object = try translate.create_object(
277
- env,
278
- "Failed to create result object",
279
- );
280
-
281
- try translate.u32_into_object(
282
- env,
283
- napi_object,
284
- "index",
285
- result.index,
286
- "Failed to set property \"index\" of result.",
287
- );
288
-
289
- try translate.u32_into_object(
290
- env,
291
- napi_object,
292
- "result",
293
- @intFromEnum(result.result),
294
- "Failed to set property \"result\" of result.",
295
- );
296
-
297
- try translate.set_array_element(
298
- env,
299
- napi_array,
300
- i,
301
- napi_object,
302
- "Failed to set element in results array.",
303
- );
304
- }
305
- },
306
- Account => {
307
- var i: u32 = 0;
308
- while (i < results.len) : (i += 1) {
309
- const result = results[i];
310
- const napi_object = try translate.create_object(
311
- env,
312
- "Failed to create account lookup result object.",
313
- );
314
-
315
- try translate.u128_into_object(
316
- env,
317
- napi_object,
318
- "id",
319
- result.id,
320
- "Failed to set property \"id\" of account lookup result.",
321
- );
322
-
323
- try translate.u128_into_object(
324
- env,
325
- napi_object,
326
- "debits_pending",
327
- result.debits_pending,
328
- "Failed to set property \"debits_pending\" of account lookup result.",
329
- );
330
-
331
- try translate.u128_into_object(
332
- env,
333
- napi_object,
334
- "debits_posted",
335
- result.debits_posted,
336
- "Failed to set property \"debits_posted\" of account lookup result.",
337
- );
338
-
339
- try translate.u128_into_object(
340
- env,
341
- napi_object,
342
- "credits_pending",
343
- result.credits_pending,
344
- "Failed to set property \"credits_pending\" of account lookup result.",
345
- );
346
-
347
- try translate.u128_into_object(
348
- env,
349
- napi_object,
350
- "credits_posted",
351
- result.credits_posted,
352
- "Failed to set property \"credits_posted\" of account lookup result.",
353
- );
354
-
355
- try translate.u128_into_object(
356
- env,
357
- napi_object,
358
- "user_data_128",
359
- result.user_data_128,
360
- "Failed to set property \"user_data_128\" of account lookup result.",
361
- );
362
-
363
- try translate.u64_into_object(
364
- env,
365
- napi_object,
366
- "user_data_64",
367
- result.user_data_64,
368
- "Failed to set property \"user_data_64\" of account lookup result.",
369
- );
370
-
371
- try translate.u32_into_object(
372
- env,
373
- napi_object,
374
- "user_data_32",
375
- result.user_data_32,
376
- "Failed to set property \"user_data_32\" of account lookup result.",
377
- );
378
-
379
- try translate.u32_into_object(
380
- env,
381
- napi_object,
382
- "reserved",
383
- result.reserved,
384
- "Failed to set property \"reserved\" of account lookup result.",
385
- );
386
-
387
- try translate.u32_into_object(
388
- env,
389
- napi_object,
390
- "ledger",
391
- @as(u32, @intCast(result.ledger)),
392
- "Failed to set property \"ledger\" of account lookup result.",
393
- );
394
-
395
- try translate.u16_into_object(
396
- env,
397
- napi_object,
398
- "code",
399
- @as(u16, @intCast(result.code)),
400
- "Failed to set property \"code\" of account lookup result.",
401
- );
402
-
403
- try translate.u16_into_object(
404
- env,
405
- napi_object,
406
- "flags",
407
- @as(u16, @bitCast(result.flags)),
408
- "Failed to set property \"flags\" of account lookup result.",
409
- );
410
-
411
- try translate.u64_into_object(
412
- env,
413
- napi_object,
414
- "timestamp",
415
- result.timestamp,
416
- "Failed to set property \"timestamp\" of account lookup result.",
417
- );
418
-
419
- try translate.set_array_element(
420
- env,
421
- napi_array,
422
- i,
423
- napi_object,
424
- "Failed to set element in results array.",
425
- );
426
- }
427
- },
428
- Transfer => {
429
- var i: u32 = 0;
430
- while (i < results.len) : (i += 1) {
431
- const result = results[i];
432
- const napi_object = try translate.create_object(
433
- env,
434
- "Failed to create transfer lookup result object.",
435
- );
436
-
437
- try translate.u128_into_object(
438
- env,
439
- napi_object,
440
- "id",
441
- result.id,
442
- "Failed to set property \"id\" of transfer lookup result.",
443
- );
444
-
445
- try translate.u128_into_object(
446
- env,
447
- napi_object,
448
- "debit_account_id",
449
- result.debit_account_id,
450
- "Failed to set property \"debit_account_id\" of transfer lookup result.",
451
- );
452
-
453
- try translate.u128_into_object(
454
- env,
455
- napi_object,
456
- "credit_account_id",
457
- result.credit_account_id,
458
- "Failed to set property \"credit_account_id\" of transfer lookup result.",
459
- );
460
-
461
- try translate.u128_into_object(
462
- env,
463
- napi_object,
464
- "amount",
465
- result.amount,
466
- "Failed to set property \"amount\" of transfer lookup result.",
467
- );
468
-
469
- try translate.u128_into_object(
470
- env,
471
- napi_object,
472
- "pending_id",
473
- result.pending_id,
474
- "Failed to set property \"pending_id\" of transfer lookup result.",
475
- );
476
-
477
- try translate.u128_into_object(
478
- env,
479
- napi_object,
480
- "user_data_128",
481
- result.user_data_128,
482
- "Failed to set property \"user_data_128\" of transfer lookup result.",
483
- );
484
-
485
- try translate.u64_into_object(
486
- env,
487
- napi_object,
488
- "user_data_64",
489
- result.user_data_64,
490
- "Failed to set property \"user_data_64\" of transfer lookup result.",
491
- );
492
-
493
- try translate.u32_into_object(
494
- env,
495
- napi_object,
496
- "user_data_32",
497
- result.user_data_32,
498
- "Failed to set property \"user_data_32\" of transfer lookup result.",
499
- );
500
-
501
- try translate.u32_into_object(
502
- env,
503
- napi_object,
504
- "timeout",
505
- result.timeout,
506
- "Failed to set property \"timeout\" of transfer lookup result.",
507
- );
508
-
509
- try translate.u32_into_object(
510
- env,
511
- napi_object,
512
- "ledger",
513
- @as(u32, @intCast(result.ledger)),
514
- "Failed to set property \"ledger\" of transfer lookup result.",
515
- );
516
-
517
- try translate.u16_into_object(
518
- env,
519
- napi_object,
520
- "code",
521
- @as(u16, @intCast(result.code)),
522
- "Failed to set property \"code\" of transfer lookup result.",
523
- );
524
-
525
- try translate.u16_into_object(
526
- env,
527
- napi_object,
528
- "flags",
529
- @as(u16, @bitCast(result.flags)),
530
- "Failed to set property \"flags\" of transfer lookup result.",
531
- );
532
-
533
- try translate.u64_into_object(
534
- env,
535
- napi_object,
536
- "timestamp",
537
- result.timestamp,
538
- "Failed to set property \"timestamp\" of transfer lookup result.",
539
- );
540
-
541
- try translate.set_array_element(
542
- env,
543
- napi_array,
544
- i,
545
- napi_object,
546
- "Failed to set element in results array.",
547
- );
548
- }
549
- },
550
- else => unreachable,
206
+ // Create a reference to the callback so it stay alive until the packet completes.
207
+ var callback_ref: c.napi_ref = undefined;
208
+ if (c.napi_create_reference(env, callback, 1, &callback_ref) != c.napi_ok) {
209
+ return translate.throw(env, "Failed to create reference to callback.");
551
210
  }
211
+ errdefer translate.delete_reference(env, callback_ref) catch {
212
+ std.log.warn("Failed to delete reference to callback on error.", .{});
213
+ };
552
214
 
553
- return napi_array;
554
- }
555
-
556
- /// Add-on code
557
- fn init(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
558
- var argc: usize = 1;
559
- var argv: [1]c.napi_value = undefined;
560
- if (c.napi_get_cb_info(env, info, &argc, &argv, null, null) != c.napi_ok) {
561
- translate.throw(env, "Failed to get args.") catch return null;
215
+ const array_length = try translate.array_length(env, array);
216
+ if (array_length < 1) {
217
+ return translate.throw(env, "Batch must contain at least one event.");
562
218
  }
563
- if (argc != 1) translate.throw(
564
- env,
565
- "Function init() must receive 1 argument exactly.",
566
- ) catch return null;
567
-
568
- const cluster = translate.u32_from_object(env, argv[0], "cluster_id") catch return null;
569
- const addresses = translate.slice_from_object(
570
- env,
571
- argv[0],
572
- "replica_addresses",
573
- ) catch return null;
574
219
 
575
- const allocator = std.heap.c_allocator;
220
+ const packet_data = switch (operation) {
221
+ inline else => |op| blk: {
222
+ const buffer = try BufferType(op).alloc(env, array_length);
223
+ errdefer buffer.free();
576
224
 
577
- const globals_raw = translate.globals(env) catch return null;
578
- const globals = globalsCast(globals_raw.?);
225
+ const events = buffer.events();
226
+ try decode_array(StateMachine.Event(op), env, array, events);
227
+ break :blk std.mem.sliceAsBytes(events);
228
+ },
229
+ };
579
230
 
580
- const context = Context.create(env, allocator, &globals.io, cluster, addresses) catch {
581
- // TODO: switch on err and provide more detailed messages
582
- translate.throw(env, "Failed to initialize Client.") catch return null;
231
+ packet.* = .{
232
+ .next = null,
233
+ .user_data = callback_ref,
234
+ .operation = @intFromEnum(operation),
235
+ .status = .ok,
236
+ .data_size = @intCast(packet_data.len),
237
+ .data = packet_data.ptr,
583
238
  };
584
239
 
585
- return context;
240
+ tb.submit(client, packet);
586
241
  }
587
242
 
588
- /// This function decodes and validates an array of Node objects, one-by-one, directly into an
589
- /// available message before requesting the client to send it.
590
- fn request(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
591
- var argc: usize = 4;
592
- var argv: [4]c.napi_value = undefined;
593
- if (c.napi_get_cb_info(env, info, &argc, &argv, null, null) != c.napi_ok) {
594
- translate.throw(env, "Failed to get args.") catch return null;
595
- }
596
-
597
- if (argc != 4) translate.throw(
598
- env,
599
- "Function request() requires 4 arguments exactly.",
600
- ) catch return null;
601
-
602
- const context_raw = translate.value_external(
603
- env,
604
- argv[0],
605
- "Failed to get Client Context pointer.",
606
- ) catch return null;
607
- const context = contextCast(context_raw.?) catch return null;
608
- const operation_int = translate.u32_from_value(env, argv[1], "operation") catch return null;
609
-
610
- if (!@as(vsr.Operation, @enumFromInt(operation_int)).valid(StateMachine)) {
611
- translate.throw(env, "Unknown operation.") catch return null;
612
- }
243
+ // Packet only has one size field which normally tracks `BufferType(op).events().len`.
244
+ // However, completion of the packet can write results.len < `BufferType(op).results().len`.
245
+ // Therefore, we stuff both `BufferType(op).count` and results.len into the packet's size field.
246
+ // Storing both allows reconstruction of `BufferType(op)` while knowing how many results completed.
247
+ const BufferSize = packed struct(u32) {
248
+ event_count: u16,
249
+ result_count: u16,
250
+ };
613
251
 
614
- if (context.client.messages_available == 0) {
615
- translate.throw(
616
- env,
617
- "Too many outstanding requests - message pool exhausted.",
618
- ) catch return null;
252
+ fn on_completion(
253
+ completion_ctx: usize,
254
+ client: tb.tb_client_t,
255
+ packet: *tb.tb_packet_t,
256
+ result_ptr: ?[*]const u8,
257
+ result_len: u32,
258
+ ) callconv(.C) void {
259
+ switch (packet.status) {
260
+ .ok => {},
261
+ .too_much_data => unreachable, // We limit packet data size during request().
262
+ .invalid_operation => unreachable, // We check the operation during request().
263
+ .invalid_data_size => unreachable, // We set correct data size during request().
619
264
  }
620
- const message = context.client.get_message();
621
- errdefer context.client.release(message);
622
-
623
- const operation = @as(Operation, @enumFromInt(@as(u8, @intCast(operation_int))));
624
- const body_length = decode_events(
625
- env,
626
- argv[2],
627
- operation,
628
- message.buffer[@sizeOf(Header)..],
629
- ) catch |err| switch (err) {
630
- error.ExceptionThrown => return null,
631
- };
632
-
633
- // This will create a reference (in V8) to the user's JS callback that we must eventually also
634
- // free in order to avoid a leak. We therefore do this last to ensure we cannot fail after
635
- // taking this reference.
636
- const user_data = translate.user_data_from_value(env, argv[3]) catch return null;
637
- context.client.request(@as(u128, @bitCast(user_data)), on_result, operation, message, body_length);
638
-
639
- return null;
640
- }
641
265
 
642
- /// The batch has already been encoded into a byte slice. This means that we only have to do one
643
- /// copy directly into an available message. No validation of the encoded data is performed except
644
- /// that it will fit into the message buffer.
645
- fn raw_request(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
646
- var argc: usize = 4;
647
- var argv: [4]c.napi_value = undefined;
648
- if (c.napi_get_cb_info(env, info, &argc, &argv, null, null) != c.napi_ok) {
649
- translate.throw(env, "Failed to get args.") catch return null;
266
+ switch (@as(Operation, @enumFromInt(packet.operation))) {
267
+ inline else => |op| {
268
+ const event_count = @divExact(packet.data_size, @sizeOf(StateMachine.Event(op)));
269
+ const buffer: BufferType(op) = .{
270
+ .ptr = @ptrCast(packet.data.?),
271
+ .count = event_count,
272
+ };
273
+
274
+ const Result = StateMachine.Result(op);
275
+ const results: []const Result = @alignCast(std.mem.bytesAsSlice(
276
+ Result,
277
+ result_ptr.?[0..result_len],
278
+ ));
279
+ @memcpy(buffer.results()[0..results.len], results);
280
+
281
+ packet.data_size = @bitCast(BufferSize{
282
+ .event_count = @intCast(event_count),
283
+ .result_count = @intCast(results.len),
284
+ });
285
+ },
650
286
  }
651
287
 
652
- if (argc != 4) translate.throw(
653
- env,
654
- "Function request() requires 4 arguments exactly.",
655
- ) catch return null;
656
-
657
- const context_raw = translate.value_external(
658
- env,
659
- argv[0],
660
- "Failed to get Client Context pointer.",
661
- ) catch return null;
662
- const context = contextCast(context_raw.?) catch return null;
663
- const operation_int = translate.u32_from_value(env, argv[1], "operation") catch return null;
664
-
665
- if (!@as(vsr.Operation, @enumFromInt(operation_int)).valid(StateMachine)) {
666
- translate.throw(env, "Unknown operation.") catch return null;
667
- }
668
- const operation = @as(Operation, @enumFromInt(@as(u8, @intCast(operation_int))));
288
+ // Stuff client pointer into packet.next to store it until the packet arrives on the JS thread.
289
+ @as(*usize, @ptrCast(&packet.next)).* = @intFromPtr(client);
669
290
 
670
- if (context.client.messages_available == 0) {
671
- translate.throw(
672
- env,
673
- "Too many outstanding requests - message pool exhausted.",
674
- ) catch return null;
291
+ // Queue the packet to be processed on the JS thread to invoke its JS callback.
292
+ const completion_tsfn: c.napi_threadsafe_function = @ptrFromInt(completion_ctx);
293
+ switch (c.napi_call_threadsafe_function(completion_tsfn, packet, c.napi_tsfn_nonblocking)) {
294
+ c.napi_ok => {},
295
+ c.napi_queue_full => @panic("ThreadSafe Function queue is full when created with no limit."),
296
+ else => unreachable,
675
297
  }
676
- const message = context.client.get_message();
677
- errdefer context.client.release(message);
298
+ }
678
299
 
679
- const body_length = translate.bytes_from_buffer(
680
- env,
681
- argv[2],
682
- message.buffer[@sizeOf(Header)..],
683
- "raw_batch",
684
- ) catch |err| switch (err) {
685
- error.ExceptionThrown => return null,
300
+ fn on_completion_js(
301
+ env: c.napi_env,
302
+ unused_js_cb: c.napi_value,
303
+ unused_context: ?*anyopaque,
304
+ packet_argument: ?*anyopaque,
305
+ ) callconv(.C) void {
306
+ _ = unused_js_cb;
307
+ _ = unused_context;
308
+
309
+ const packet: *tb.tb_packet_t = @ptrCast(@alignCast(packet_argument.?));
310
+ assert(packet.status == .ok);
311
+
312
+ // Decode the packet's Buffer results into an array then free the Buffer.
313
+ const array_or_error = switch (@as(Operation, @enumFromInt(packet.operation))) {
314
+ inline else => |op| blk: {
315
+ const buffer_size: BufferSize = @bitCast(packet.data_size);
316
+ const buffer: BufferType(op) = .{
317
+ .ptr = @ptrCast(packet.data.?),
318
+ .count = buffer_size.event_count,
319
+ };
320
+ defer buffer.free();
321
+
322
+ const results = buffer.results()[0..buffer_size.result_count];
323
+ break :blk encode_array(StateMachine.Result(op), env, results);
324
+ },
686
325
  };
687
326
 
688
- // This will create a reference (in V8) to the user's JS callback that we must eventually also
689
- // free in order to avoid a leak. We therefore do this last to ensure we cannot fail after
690
- // taking this reference.
691
- const user_data = translate.user_data_from_value(env, argv[3]) catch return null;
692
- context.client.request(@as(u128, @bitCast(user_data)), on_result, operation, message, body_length);
693
-
694
- return null;
695
- }
327
+ // Extract the remaining packet information and release it back to the client.
328
+ const client: tb.tb_client_t = @ptrFromInt(@as(*usize, @ptrCast(&packet.next)).*);
329
+ const callback_ref: c.napi_ref = @ptrCast(@alignCast(packet.user_data.?));
330
+ tb.release_packet(client, packet);
331
+
332
+ // Parse Result array out of packet data, freeing it in the process.
333
+ // NOTE: Ensure this is called before anything that could early-return to avoid a alloc leak.
334
+ var callback_error = napi_null;
335
+ const callback_result = array_or_error catch |err| switch (err) {
336
+ error.ExceptionThrown => blk: {
337
+ if (c.napi_get_and_clear_last_exception(env, &callback_error) != c.napi_ok) {
338
+ std.log.warn("Failed to capture callback error from thrown Exception.", .{});
339
+ }
340
+ break :blk napi_null;
341
+ },
342
+ };
696
343
 
697
- fn on_result(user_data: u128, operation: Operation, results: []const u8) void {
698
- // A reference to the user's JS callback was made in `request` or `raw_request`. This MUST be
699
- // cleaned up regardless of the result of this function.
700
- const env = @as(translate.UserData, @bitCast(user_data)).env;
701
- const callback_reference = @as(translate.UserData, @bitCast(user_data)).callback_reference;
702
- defer translate.delete_reference(env, callback_reference) catch {
703
- std.log.warn("on_result: Failed to delete reference to user's JS callback.", .{});
344
+ // Make sure to delete the callback reference once we're done calling it.
345
+ defer if (c.napi_delete_reference(env, callback_ref) != c.napi_ok) {
346
+ std.log.warn("Failed to delete reference to user's JS callback.", .{});
704
347
  };
705
348
 
706
- const napi_callback = translate.reference_value(
707
- env,
708
- callback_reference,
709
- "Failed to get callback reference.",
710
- ) catch return;
711
- const scope = translate.scope(
349
+ const callback = translate.reference_value(
712
350
  env,
713
- "Failed to get \"this\" for results callback.",
351
+ callback_ref,
352
+ "Failed to get callback from reference.",
714
353
  ) catch return;
715
- const globals_raw = translate.globals(env) catch return;
716
- const globals = globalsCast(globals_raw.?);
717
- const argc: usize = 2;
718
- var argv: [argc]c.napi_value = undefined;
719
-
720
- const napi_results = switch (operation) {
721
- .create_accounts => encode_napi_results_array(
722
- CreateAccountsResult,
723
- env,
724
- results,
725
- ) catch return,
726
- .create_transfers => encode_napi_results_array(
727
- CreateTransfersResult,
728
- env,
729
- results,
730
- ) catch return,
731
- .lookup_accounts => encode_napi_results_array(Account, env, results) catch return,
732
- .lookup_transfers => encode_napi_results_array(Transfer, env, results) catch return,
733
- };
734
354
 
735
- argv[0] = globals.napi_undefined;
736
- argv[1] = napi_results;
737
-
738
- translate.call_function(env, scope, napi_callback, argc, argv[0..]) catch {
739
- translate.throw(env, "Failed to call JS results callback.") catch return;
740
- };
355
+ var args = [_]c.napi_value{ callback_error, callback_result };
356
+ _ = translate.call_function(env, napi_null, callback, &args) catch return;
741
357
  }
742
358
 
743
- fn tick(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
744
- var argc: usize = 1;
745
- var argv: [1]c.napi_value = undefined;
746
- if (c.napi_get_cb_info(env, info, &argc, &argv, null, null) != c.napi_ok) {
747
- translate.throw(env, "Failed to get args.") catch return null;
359
+ // (De)Serialization
360
+
361
+ fn decode_array(comptime Event: type, env: c.napi_env, array: c.napi_value, events: []Event) !void {
362
+ for (events, 0..) |*event, i| {
363
+ const object = try translate.array_element(env, array, @intCast(i));
364
+ switch (Event) {
365
+ Account, Transfer => {
366
+ inline for (std.meta.fields(Event)) |field| {
367
+ const FieldInt = switch (@typeInfo(field.type)) {
368
+ .Struct => |info| info.backing_integer.?,
369
+ else => field.type,
370
+ };
371
+
372
+ const value = try @field(translate, @typeName(FieldInt) ++ "_from_object")(
373
+ env,
374
+ object,
375
+ @ptrCast(field.name ++ "\x00"),
376
+ );
377
+
378
+ if (std.mem.eql(u8, field.name, "timestamp") and value != 0) {
379
+ return translate.throw(
380
+ env,
381
+ "Timestamp should be set to 0 as this will be set correctly by Replica.",
382
+ );
383
+ }
384
+
385
+ @field(event, field.name) = switch (@typeInfo(field.type)) {
386
+ .Struct => @as(field.type, @bitCast(value)),
387
+ else => value,
388
+ };
389
+ }
390
+ },
391
+ u128 => event.* = try translate.u128_from_value(env, object, "lookup"),
392
+ else => @compileError("invalid Event type"),
393
+ }
748
394
  }
395
+ }
749
396
 
750
- if (argc != 1) translate.throw(
397
+ fn encode_array(comptime Result: type, env: c.napi_env, results: []const Result) !c.napi_value {
398
+ const array = try translate.create_array(
751
399
  env,
752
- "Function tick() requires 1 argument exactly.",
753
- ) catch return null;
400
+ @intCast(results.len),
401
+ "Failed to allocate array for results.",
402
+ );
754
403
 
755
- const context_raw = translate.value_external(
756
- env,
757
- argv[0],
758
- "Failed to get Client Context pointer.",
759
- ) catch return null;
760
- const context = contextCast(context_raw.?) catch return null;
404
+ for (results, 0..) |*result, i| {
405
+ const object = try translate.create_object(
406
+ env,
407
+ "Failed to create " ++ @typeName(Result) ++ " object.",
408
+ );
761
409
 
762
- context.client.tick();
763
- context.io.tick() catch |err| switch (err) {
764
- // TODO exhaustive switch
765
- else => {
766
- translate.throw(env, "Failed to tick IO.") catch return null;
767
- },
768
- };
769
- return null;
410
+ inline for (std.meta.fields(Result)) |field| {
411
+ const FieldInt = switch (@typeInfo(field.type)) {
412
+ .Struct => |info| info.backing_integer.?,
413
+ .Enum => |info| info.tag_type,
414
+ else => field.type,
415
+ };
416
+
417
+ const value: FieldInt = switch (@typeInfo(field.type)) {
418
+ .Struct => @bitCast(@field(result, field.name)),
419
+ .Enum => @intFromEnum(@field(result, field.name)),
420
+ else => @field(result, field.name),
421
+ };
422
+
423
+ try @field(translate, @typeName(FieldInt) ++ "_into_object")(
424
+ env,
425
+ object,
426
+ @ptrCast(field.name ++ "\x00"),
427
+ value,
428
+ "Failed to set property \"" ++ field.name ++ "\" of " ++ @typeName(Result) ++ " object",
429
+ );
430
+
431
+ try translate.set_array_element(
432
+ env,
433
+ array,
434
+ @intCast(i),
435
+ object,
436
+ "Failed to set element in results array.",
437
+ );
438
+ }
439
+ }
440
+
441
+ return array;
770
442
  }
771
443
 
772
- fn deinit(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
773
- var argc: usize = 1;
774
- var argv: [1]c.napi_value = undefined;
775
- if (c.napi_get_cb_info(env, info, &argc, &argv, null, null) != c.napi_ok) {
776
- translate.throw(env, "Failed to get args.") catch return null;
777
- }
444
+ /// Each packet allocates enough room to hold both its Events and its Results.
445
+ /// Buffer is an abstraction over the memory management for this.
446
+ fn BufferType(comptime op: Operation) type {
447
+ return struct {
448
+ const Buffer = @This();
449
+ const Event = StateMachine.Event(op);
450
+ const Result = StateMachine.Result(op);
451
+ const max_align: u29 = @max(@alignOf(Event), @alignOf(Result));
452
+
453
+ ptr: [*]u8,
454
+ count: u32,
455
+
456
+ fn alloc(env: c.napi_env, count: u32) !Buffer {
457
+ // Allocate enough bytes to hold memory for the Events and the Results.
458
+ const max_bytes = @max(@sizeOf(Event) * count, @sizeOf(Result) * count);
459
+ if (@sizeOf(vsr.Header) + max_bytes > constants.message_size_max) {
460
+ return translate.throw(env, "Batch is larger than the maximum message size.");
461
+ }
778
462
 
779
- if (argc != 1) translate.throw(
780
- env,
781
- "Function deinit() requires 1 argument exactly.",
782
- ) catch return null;
463
+ const bytes = allocator.alignedAlloc(u8, max_align, max_bytes) catch |e| switch (e) {
464
+ error.OutOfMemory => return translate.throw(env, "Batch allocation ran out of memory."),
465
+ };
466
+ errdefer allocator.free(bytes);
783
467
 
784
- const context_raw = translate.value_external(
785
- env,
786
- argv[0],
787
- "Failed to get Client Context pointer.",
788
- ) catch return null;
789
- const context = contextCast(context_raw.?) catch return null;
468
+ return Buffer{
469
+ .ptr = bytes.ptr,
470
+ .count = count,
471
+ };
472
+ }
473
+
474
+ fn free(buffer: Buffer) void {
475
+ const max_bytes = @max(@sizeOf(Event) * buffer.count, @sizeOf(Result) * buffer.count);
476
+ const bytes: []align(max_align) u8 = @alignCast(buffer.ptr[0..max_bytes]);
477
+ allocator.free(bytes);
478
+ }
790
479
 
791
- const allocator = std.heap.c_allocator;
792
- context.client.deinit(allocator);
793
- context.message_pool.deinit(allocator);
794
- allocator.free(context.addresses);
480
+ fn events(buffer: Buffer) []Event {
481
+ const event_bytes = buffer.ptr[0 .. @sizeOf(Event) * buffer.count];
482
+ return @alignCast(std.mem.bytesAsSlice(Event, event_bytes));
483
+ }
795
484
 
796
- return null;
485
+ fn results(buffer: Buffer) []Result {
486
+ const result_bytes = buffer.ptr[0 .. @sizeOf(Result) * buffer.count];
487
+ return @alignCast(std.mem.bytesAsSlice(Result, result_bytes));
488
+ }
489
+ };
797
490
  }