apache-iggy 0.10.0-edge.6 → 0.10.0-edge.7

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.
@@ -28,9 +28,8 @@ export type SendMessagesConfirmation = {
28
28
  *
29
29
  * Delivery is at-least-once, so an earlier retry of the same batch may
30
30
  * already have committed at a lower offset: this never identifies a batch
31
- * uniquely. A batch is confirmed once it is committed in memory, not once it
32
- * is fsynced, so a crash-restart can stamp a later batch with an offset a
33
- * client has already recorded.
31
+ * uniquely. Confirmation follows VSR quorum commit. Persisted message
32
+ * durability also requires recoverable stable-storage copies on the quorum.
34
33
  */
35
34
  baseOffset: bigint;
36
35
  };
@@ -28,22 +28,25 @@ const prefixed = (block) => {
28
28
  *
29
29
  * Rust pins the identical bytes in `core/binary_protocol/src/primitives/options.rs`,
30
30
  * as do the Go and Java SDKs. Round-tripping through this SDK's own decoder proves
31
- * nothing about interoperability; these bytes are the contract.
31
+ * nothing about interoperability. This insertion-order vector covers Bool, Uint64
32
+ * and String values, independently of Rust's sorted map order.
32
33
  */
33
34
  const GOLDEN_OPTIONS_BLOCK = Buffer.from([
34
- 2, 13, 0, 0, 0,
35
- ...Buffer.from('enforce_fsync'),
35
+ 2, 20, 0, 0, 0,
36
+ ...Buffer.from('preallocate_segments'),
36
37
  3, 1, 0, 0, 0, 1,
37
38
  2, 12, 0, 0, 0,
38
39
  ...Buffer.from('segment_size'),
39
40
  12, 8, 0, 0, 0,
40
- 0, 0, 0, 64, 0, 0, 0, 0
41
+ 0, 0, 0, 64, 0, 0, 0, 0,
42
+ 2, 10, 0, 0, 0, 100, 117, 114, 97, 98, 105, 108, 105, 116, 121, 2, 9, 0, 0, 0, 112, 101, 114, 115, 105, 115, 116, 101, 100
41
43
  ]);
42
44
  describe('serializeOptions', () => {
43
45
  it('encodes the cross-SDK golden vector byte for byte', () => {
44
46
  const encoded = serializeOptions([
45
- { key: 'enforce_fsync', value: HeaderValue.Bool(true) },
46
- { key: 'segment_size', value: HeaderValue.Uint64(1073741824n) }
47
+ { key: 'preallocate_segments', value: HeaderValue.Bool(true) },
48
+ { key: 'segment_size', value: HeaderValue.Uint64(1073741824n) },
49
+ { key: 'durability', value: HeaderValue.String('persisted') }
47
50
  ]);
48
51
  assert.deepEqual(encoded, GOLDEN_OPTIONS_BLOCK);
49
52
  });
@@ -1,7 +1,7 @@
1
1
  import type { CommandResponse } from '../../client/client.type.js';
2
2
  import { type Id } from '../identifier.utils.js';
3
3
  import { type OptionEntry } from '../options.utils.js';
4
- import { type Topic, type CompressionAlgorithm as CompressionAlgorithmT } from './topic.utils.js';
4
+ import { Durability, type Topic, type CompressionAlgorithm as CompressionAlgorithmT } from './topic.utils.js';
5
5
  /**
6
6
  * Parameters for the create topic command.
7
7
  */
@@ -28,8 +28,9 @@ export type CreateTopic = {
28
28
  maxTopicSize?: bigint;
29
29
  /** Segment size in bytes: 512-byte multiple between 1 MiB and 1 GiB */
30
30
  segmentSize?: bigint;
31
- /** Fsync every write instead of leaving it to the page cache */
32
- enforceFsync?: boolean;
31
+ /** Message completion policy. Defaults to replicated, independently of offsets. */
32
+ durability?: Durability;
33
+ consumerOffsetDurability?: Durability;
33
34
  /** Message count that triggers a save (must be non-zero) */
34
35
  messagesRequiredToSave?: number;
35
36
  /** Accumulated message bytes that trigger a save */
@@ -52,7 +53,7 @@ export type CreateTopic = {
52
53
  */
53
54
  export declare const CREATE_TOPIC: {
54
55
  code: number;
55
- serialize: ({ streamId, name, partitionCount, compressionAlgorithm, messageExpiry, maxTopicSize, segmentSize, enforceFsync, messagesRequiredToSave, sizeOfMessagesRequiredToSave, preallocateSegments, options: extraOptions }: CreateTopic) => Buffer<ArrayBuffer>;
56
+ serialize: ({ streamId, name, partitionCount, compressionAlgorithm, messageExpiry, maxTopicSize, segmentSize, durability, consumerOffsetDurability, messagesRequiredToSave, sizeOfMessagesRequiredToSave, preallocateSegments, options: extraOptions }: CreateTopic) => Buffer<ArrayBuffer>;
56
57
  deserialize: (r: CommandResponse) => Topic;
57
58
  };
58
59
  /**
@@ -19,7 +19,7 @@ import { wrapCommand } from '../command.utils.js';
19
19
  import { COMMAND_CODE } from '../command.code.js';
20
20
  import { dedupeOptions, serializeOptions } from '../options.utils.js';
21
21
  import { HeaderValue } from '../message/header.utils.js';
22
- import { isValidCompressionAlgorithm, CompressionAlgorithm, compressionAlgorithmName, deserializeTopic } from './topic.utils.js';
22
+ import { isValidCompressionAlgorithm, CompressionAlgorithm, Durability, compressionAlgorithmName, deserializeTopic } from './topic.utils.js';
23
23
  /**
24
24
  * Create topic command definition.
25
25
  * Creates a new topic within a stream.
@@ -28,7 +28,7 @@ import { isValidCompressionAlgorithm, CompressionAlgorithm, compressionAlgorithm
28
28
  */
29
29
  export const CREATE_TOPIC = {
30
30
  code: COMMAND_CODE.CreateTopic,
31
- serialize: ({ streamId, name, partitionCount, compressionAlgorithm = CompressionAlgorithm.None, messageExpiry = 0n, maxTopicSize = 0n, segmentSize, enforceFsync, messagesRequiredToSave, sizeOfMessagesRequiredToSave, preallocateSegments, options: extraOptions = [] }) => {
31
+ serialize: ({ streamId, name, partitionCount, compressionAlgorithm = CompressionAlgorithm.None, messageExpiry = 0n, maxTopicSize = 0n, segmentSize, durability = Durability.Replicated, consumerOffsetDurability = Durability.Replicated, messagesRequiredToSave, sizeOfMessagesRequiredToSave, preallocateSegments, options: extraOptions = [] }) => {
32
32
  // Topic ID is now auto-assigned by the server, not sent in the protocol
33
33
  const streamIdentifier = serializeIdentifier(streamId);
34
34
  const bName = Buffer.from(name);
@@ -60,10 +60,19 @@ export const CREATE_TOPIC = {
60
60
  options.push({
61
61
  key: 'segment_size', value: HeaderValue.Uint64(segmentSize)
62
62
  });
63
- if (enforceFsync !== undefined)
64
- options.push({
65
- key: 'enforce_fsync', value: HeaderValue.Bool(enforceFsync)
66
- });
63
+ for (const [key, policy] of [
64
+ ['durability', durability],
65
+ ['consumer_offset_durability', consumerOffsetDurability]
66
+ ]) {
67
+ if (policy !== Durability.Replicated && policy !== Durability.Persisted)
68
+ throw new Error(`Invalid ${key}: ${policy}`);
69
+ const expected = HeaderValue.String(policy);
70
+ for (const entry of extraOptions) {
71
+ if (entry.key === key && (entry.value.kind !== expected.kind || entry.value.value !== expected.value))
72
+ throw new Error(`Conflicting ${key}`);
73
+ }
74
+ options.push({ key, value: expected });
75
+ }
67
76
  if (messagesRequiredToSave !== undefined)
68
77
  options.push({
69
78
  key: 'messages_required_to_save',
@@ -16,6 +16,7 @@
16
16
  // under the License.
17
17
  import { describe, it } from 'node:test';
18
18
  import assert from 'node:assert/strict';
19
+ import { Durability } from './topic.utils.js';
19
20
  import { CREATE_TOPIC } from './create-topic.command.js';
20
21
  import { deserializeOptions } from '../options.utils.js';
21
22
  import { HeaderValue } from '../message/header.utils.js';
@@ -32,17 +33,19 @@ describe('CreateTopic', () => {
32
33
  // TLV field: [kind:u8][len:u32_le][bytes]
33
34
  const tlvSize = (bytes) => 1 + 4 + bytes;
34
35
  const identifierSize = 1 + 1 + 4; // numeric stream id
36
+ const defaultPolicySize = tlvSize('durability'.length) + tlvSize('replicated'.length)
37
+ + tlvSize('consumer_offset_durability'.length) + tlvSize('replicated'.length);
35
38
  const fixedSize = identifierSize + 4 + 1; // + partitions_count + name_len
36
39
  it('serialize name and default options into buffer', () => {
37
- // Server-default sentinels are omitted, leaving an empty options block.
38
- assert.deepEqual(CREATE_TOPIC.serialize(t1).length, fixedSize + t1.name.length);
40
+ // Durability defaults are explicit, while the other sentinels are omitted.
41
+ assert.deepEqual(CREATE_TOPIC.serialize(t1).length, fixedSize + t1.name.length + defaultPolicySize);
39
42
  });
40
43
  it('serialize partitionCount as a fixed u32 before the name', () => {
41
44
  const t = { ...t1, partitionCount: 7 };
42
45
  const b = CREATE_TOPIC.serialize(t);
43
46
  assert.equal(b.readUInt32LE(identifierSize), 7);
44
47
  assert.equal(b.readUInt8(identifierSize + 4), t.name.length);
45
- assert.equal(b.subarray(fixedSize).toString(), t.name);
48
+ assert.equal(b.subarray(fixedSize, fixedSize + t.name.length).toString(), t.name);
46
49
  });
47
50
  it('serialize non-default options into buffer', () => {
48
51
  const t = {
@@ -51,7 +54,7 @@ describe('CreateTopic', () => {
51
54
  messageExpiry: 42n,
52
55
  maxTopicSize: 1024n
53
56
  };
54
- assert.deepEqual(CREATE_TOPIC.serialize(t).length, fixedSize + t1.name.length
57
+ assert.deepEqual(CREATE_TOPIC.serialize(t).length, fixedSize + t1.name.length + defaultPolicySize
55
58
  + tlvSize('compression_algorithm'.length) + tlvSize('gzip'.length)
56
59
  + tlvSize('message_expiry'.length) + tlvSize(8)
57
60
  + tlvSize('max_topic_size'.length) + tlvSize(8));
@@ -60,14 +63,14 @@ describe('CreateTopic', () => {
60
63
  const t = {
61
64
  ...t1,
62
65
  segmentSize: 1048576n,
63
- enforceFsync: true,
66
+ durability: Durability.Persisted,
64
67
  messagesRequiredToSave: 1000,
65
68
  sizeOfMessagesRequiredToSave: 4096n,
66
69
  preallocateSegments: false
67
70
  };
68
- assert.deepEqual(CREATE_TOPIC.serialize(t).length, fixedSize + t1.name.length
71
+ assert.deepEqual(CREATE_TOPIC.serialize(t).length, fixedSize + t1.name.length + defaultPolicySize
69
72
  + tlvSize('segment_size'.length) + tlvSize(8)
70
- + tlvSize('enforce_fsync'.length) + tlvSize(1)
73
+ + 'persisted'.length - 'replicated'.length
71
74
  + tlvSize('messages_required_to_save'.length) + tlvSize(4)
72
75
  + tlvSize('size_of_messages_required_to_save'.length) + tlvSize(8)
73
76
  + tlvSize('preallocate_segments'.length) + tlvSize(1));
@@ -77,7 +80,7 @@ describe('CreateTopic', () => {
77
80
  ...t1,
78
81
  maxTopicSize: 4096n,
79
82
  options: [
80
- { key: 'enforce_fsync', value: HeaderValue.Bool(true) },
83
+ { key: 'preallocate_segments', value: HeaderValue.Bool(true) },
81
84
  // The typed field covers this key, so the caller's entry is dropped:
82
85
  // a duplicate key makes the server refuse the whole block.
83
86
  { key: 'max_topic_size', value: HeaderValue.String('1 GiB') }
@@ -86,10 +89,27 @@ describe('CreateTopic', () => {
86
89
  const b = CREATE_TOPIC.serialize(t);
87
90
  // The create payload runs its options block to the end, unprefixed.
88
91
  const options = deserializeOptions(b, fixedSize + t.name.length);
89
- assert.deepEqual(Object.keys(options).sort(), ['enforce_fsync', 'max_topic_size']);
90
- assert.equal(options.enforce_fsync, true);
92
+ assert.deepEqual(Object.keys(options).sort(), ['consumer_offset_durability', 'durability', 'max_topic_size', 'preallocate_segments']);
93
+ assert.equal(options.preallocate_segments, true);
91
94
  assert.equal(options.max_topic_size, 4096n);
92
95
  });
96
+ it('keeps each omitted durability policy replicated', () => {
97
+ for (const selected of [
98
+ { durability: Durability.Persisted },
99
+ { consumerOffsetDurability: Durability.Persisted }
100
+ ]) {
101
+ const input = { ...t1, ...selected };
102
+ const encoded = CREATE_TOPIC.serialize(input);
103
+ const options = deserializeOptions(encoded, fixedSize + input.name.length);
104
+ assert.equal(options.durability, selected.durability ?? 'replicated');
105
+ assert.equal(options.consumer_offset_durability, selected.consumerOffsetDurability ?? 'replicated');
106
+ }
107
+ });
108
+ it('rejects a conflicting raw durability instead of weakening it', () => {
109
+ assert.throws(() => CREATE_TOPIC.serialize({ ...t1, options: [
110
+ { key: 'durability', value: HeaderValue.String('persisted') }
111
+ ] }));
112
+ });
93
113
  it('throw on name < 1', () => {
94
114
  const t = { ...t1, name: '' };
95
115
  assert.throws(() => CREATE_TOPIC.serialize(t));
@@ -5,5 +5,5 @@ export * from './get-topics.command.js';
5
5
  export * from './purge-topic.command.js';
6
6
  export * from './update-topic.command.js';
7
7
  export * from './ensure-topic.virtual.command.js';
8
- export { CompressionAlgorithm } from './topic.utils.js';
8
+ export { CompressionAlgorithm, Durability } from './topic.utils.js';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -21,5 +21,5 @@ export * from './get-topics.command.js';
21
21
  export * from './purge-topic.command.js';
22
22
  export * from './update-topic.command.js';
23
23
  export * from './ensure-topic.virtual.command.js';
24
- export { CompressionAlgorithm } from './topic.utils.js';
24
+ export { CompressionAlgorithm, Durability } from './topic.utils.js';
25
25
  //# sourceMappingURL=index.js.map
@@ -138,5 +138,10 @@ export declare const deserializeTopic: (p: Buffer, pos?: number) => TopicSeriali
138
138
  * @returns Array of deserialized topics
139
139
  */
140
140
  export declare const deserializeTopics: (p: Buffer, pos?: number) => Topic[];
141
+ export declare const Durability: {
142
+ readonly Replicated: "replicated";
143
+ readonly Persisted: "persisted";
144
+ };
145
+ export type Durability = typeof Durability[keyof typeof Durability];
141
146
  export {};
142
147
  //# sourceMappingURL=topic.utils.d.ts.map
@@ -146,4 +146,8 @@ export const deserializeTopics = (p, pos = 0) => {
146
146
  }
147
147
  return topics;
148
148
  };
149
+ export const Durability = {
150
+ Replicated: 'replicated',
151
+ Persisted: 'persisted'
152
+ };
149
153
  //# sourceMappingURL=topic.utils.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "apache-iggy",
3
3
  "type": "module",
4
- "version": "0.10.0-edge.6",
4
+ "version": "0.10.0-edge.7",
5
5
  "description": "Official Apache Iggy NodeJS SDK",
6
6
  "keywords": [
7
7
  "iggy",