Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 26 additions & 39 deletions test/uts/objects/integration/objects_gc.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
/**
* UTS Integration: Objects GC Tests
*
* Spec points: RTO10, RTLM19
* Spec points: RTO10, RTLM19, RTLM5d2h, RTLM7
* Source: specification/uts/objects/integration/objects_gc_test.md
*
* Behavioral verification of garbage collection for tombstoned objects and
* tombstoned map entries. The UTS spec uses ADVANCE_TIME (fake timers) to
* control timing. However, ably-js's ObjectsPool uses native setInterval
* (not Platform.Config.setTimeout), so FakeClock cannot intercept GC timers
* in an integration context with real server connections.
* Behavioral verification of tombstone semantics at the integration tier:
* removing a map entry tombstones it (RTLM7), tombstoned entries read back
* as undefined/null (RTLM5d2h), and the key is recreatable with a fresh
* server-assigned objectId — safe because tombstoned state is retained for
* the GC grace period (RTO10).
*
* Deviations:
* - RTO10/tombstoned-object-gc-recreate-0: Cannot use fake timers with real
* server connections because ably-js GC uses native setInterval. Instead,
* we test the observable behavior: remove a counter, then re-set the same
* key with a new counter. The server assigns a new objectId, confirming
* the old object is gone and a new one was created. The GC grace period
* advancement is omitted; we rely on the server accepting the new create.
* - RTO10/tombstoned-object-gc-recreate-0: UTS spec asserts value() == null
* after removal. ably-js PathObject#value() returns undefined for
* tombstoned entries. Assertion adapted accordingly.
* - RTLM19/tombstoned-entry-gc-reset-0: Same fake timer deviation as above.
* We test remove + re-set without time advancement. The server accepts the
* new MAP_SET because the entry was removed (tombstoned) and a new serial
* is assigned. GC of the tombstone entry is not directly observable in
* this integration context.
* - RTLM19/tombstoned-entry-gc-reset-0: Same undefined vs null deviation.
* The timer-based GC sweep itself (RTO10a-c, RTLM19) is verified at the
* unit tier (RTO10/RTO10b1 in test/uts/objects/unit/realtime_object.test.ts,
* RTLM19 in test/uts/objects/unit/live_map.test.ts): integration
* tests run on wall-clock time against a real server, and the sweep's
* cadence (RTO10a, ~5 minutes) with the server-provided grace period
* (RTO10b, default 24h) is not observable within test timeouts. The
* spec's undefined/null (RTLM5d2h) maps to `undefined` in ably-js.
*/

import { expect } from 'chai';
Expand All @@ -42,8 +33,9 @@ import {
pollUntil,
} from '../../realtime/integration/sandbox';
import * as LiveObjectsPlugin from '../../../../src/plugins/liveobjects';
import { describeWithProtocols } from './protocol_variants';

describe('uts/objects/integration/objects_gc', function () {
describeWithProtocols('uts/objects/integration/objects_gc', function (useBinaryProtocol) {
this.timeout(30000);

before(async function () {
Expand All @@ -55,13 +47,11 @@ describe('uts/objects/integration/objects_gc', function () {
});

/**
* RTO10 - Tombstoned object is GC'd and recreatable
* RTO10 - Tombstoned object is recreatable with new objectId
*
* After an object is tombstoned (removed from map), a new object can be
* created at the same map key. The new object gets a different objectId.
*
* Deviation: GC grace period advancement omitted (see file header).
* We test remove + re-create and verify new objectId differs from old.
* created at the same map key. The new object gets a different
* server-assigned objectId, confirming the old object is gone.
*/
// UTS: objects/integration/RTO10/tombstoned-object-gc-recreate-0
it('RTO10 - tombstoned object is recreatable with new objectId', async function () {
Expand All @@ -71,7 +61,7 @@ describe('uts/objects/integration/objects_gc', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(client);
Expand All @@ -95,7 +85,7 @@ describe('uts/objects/integration/objects_gc', function () {
// Remove it (tombstones the entry and the object)
await root.remove('counter');

// Deviation: ably-js returns undefined for tombstoned entries, not null
// RTLM5d2h: tombstoned entries read back as undefined/null (undefined in ably-js)
await pollUntil(() => (root.get('counter').value() === undefined ? true : null), {
interval: 500,
timeout: 10000,
Expand All @@ -119,14 +109,11 @@ describe('uts/objects/integration/objects_gc', function () {
});

/**
* RTLM19 - Tombstoned map entry is GC'd, re-settable
*
* After a map entry is tombstoned (removed), the entry can be re-set.
* A subsequent MAP_SET succeeds because the server processes the new
* operation against the tombstoned entry.
* RTLM19 - Tombstoned map entry is re-settable
*
* Deviation: GC grace period advancement omitted (see file header).
* We test remove + re-set and verify the value is correctly restored.
* After a map entry is tombstoned (removed, RTLM7), the entry can be
* re-set. The subsequent MAP_SET succeeds because the server assigns a
* newer serial than the removal's.
*/
// UTS: objects/integration/RTLM19/tombstoned-entry-gc-reset-0
it('RTLM19 - tombstoned map entry is re-settable', async function () {
Expand All @@ -136,7 +123,7 @@ describe('uts/objects/integration/objects_gc', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(client);
Expand All @@ -158,7 +145,7 @@ describe('uts/objects/integration/objects_gc', function () {

await root.remove('ephemeral');

// Deviation: ably-js returns undefined for tombstoned entries, not null
// RTLM5d2h: tombstoned entries read back as undefined/null (undefined in ably-js)
await pollUntil(() => (root.get('ephemeral').value() === undefined ? true : null), {
interval: 500,
timeout: 10000,
Expand Down
27 changes: 15 additions & 12 deletions test/uts/objects/integration/objects_lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import {
pollUntil,
} from '../../realtime/integration/sandbox';
import * as LiveObjectsPlugin from '../../../../src/plugins/liveobjects';
import { describeWithProtocols } from './protocol_variants';

describe('uts/objects/integration/objects_lifecycle', function () {
describeWithProtocols('uts/objects/integration/objects_lifecycle', function (useBinaryProtocol) {
this.timeout(30000);

before(async function () {
Expand All @@ -49,7 +50,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(client);
Expand Down Expand Up @@ -83,7 +84,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientA);
Expand All @@ -92,7 +93,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientB);
Expand Down Expand Up @@ -148,7 +149,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientA);
Expand All @@ -157,7 +158,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientB);
Expand Down Expand Up @@ -210,7 +211,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientA);
Expand All @@ -219,7 +220,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientB);
Expand Down Expand Up @@ -287,7 +288,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientA);
Expand All @@ -296,7 +297,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(clientB);
Expand Down Expand Up @@ -351,7 +352,9 @@ describe('uts/objects/integration/objects_lifecycle', function () {
it('RTPO15 - client syncs pre-existing data provisioned via REST', async function () {
const channelName = uniqueChannelName('objects-rest-provision');

// Provision data via REST before any realtime client connects
// Provision data via REST before any realtime client connects. The spec's
// provision_objects_via_rest helper is a raw JSON POST, so the provisioning
// client stays on json regardless of the protocol variant under test.
const restClient = new Ably.Rest({
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
Expand All @@ -370,7 +373,7 @@ describe('uts/objects/integration/objects_lifecycle', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
});
trackClient(client);
Expand Down
20 changes: 8 additions & 12 deletions test/uts/objects/integration/objects_sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import {
uniqueChannelName,
pollUntil,
} from '../../realtime/integration/sandbox';
import { describeWithProtocols } from './protocol_variants';

describe('uts/objects/integration/objects_sync', function () {
describeWithProtocols('uts/objects/integration/objects_sync', function (useBinaryProtocol) {
this.timeout(30000);

before(async function () {
Expand All @@ -49,7 +50,7 @@ describe('uts/objects/integration/objects_sync', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
} as any);
trackClient(client);
Expand Down Expand Up @@ -83,7 +84,7 @@ describe('uts/objects/integration/objects_sync', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
} as any);
trackClient(clientA);
Expand All @@ -92,7 +93,7 @@ describe('uts/objects/integration/objects_sync', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
} as any);
trackClient(clientB);
Expand Down Expand Up @@ -145,7 +146,7 @@ describe('uts/objects/integration/objects_sync', function () {
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
} as any);
trackClient(client);
Expand Down Expand Up @@ -205,21 +206,16 @@ describe('uts/objects/integration/objects_sync', function () {
* Channel attached with only OBJECT_SUBSCRIBE mode. Server
* sends HAS_OBJECTS, sync completes, root is an empty LiveMap.
*
* DEVIATION: The UTS spec title says "without OBJECT_SUBSCRIBE" but the
* pseudocode uses modes: ["OBJECT_SUBSCRIBE"]. We follow the pseudocode
* since OBJECT_SUBSCRIBE is required for channel.object.get() to work
* (RTO2a). The test verifies that get() resolves even without
* OBJECT_PUBLISH mode, and the root has no entries.
*/
// UTS: objects/integration/RTO4/attach-subscribe-only-0
it('RTO4 - attach with OBJECT_SUBSCRIBE only resolves get() with empty pool', async function () {
it('RTO4 - Attach without OBJECT_PUBLISH still resolves get() with empty pool', async function () {
const channelName = uniqueChannelName('objects-subscribe-only');

const client = new Ably.Realtime({
key: getApiKey(),
endpoint: SANDBOX_ENDPOINT,
autoConnect: false,
useBinaryProtocol: false,
useBinaryProtocol,
plugins: { LiveObjects: LiveObjectsPlugin },
} as any);
trackClient(client);
Expand Down
30 changes: 30 additions & 0 deletions test/uts/objects/integration/protocol_variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Protocol variant helper for UTS integration tests.
*
* Integration spec files declare "Protocol Variants: json, msgpack" — every
* test in the file runs once per variant with useBinaryProtocol set
* accordingly. This mirrors the legacy suite's SharedHelper.testOnJsonMsgpack
* pattern (test/common/modules/shared_helper.js) at describe level, so both
* variants always run without any command-line or environment parameter.
*/

export type Protocol = 'json' | 'msgpack';

// Structural stand-ins for mocha's Suite and global describe so this file
// needs no mocha type definitions. mocha provides describe at runtime.
interface SuiteContext {
timeout(ms: number): unknown;
}

declare function describe(title: string, fn: (this: SuiteContext) => void): unknown;

export function describeWithProtocols(
title: string,
body: (this: SuiteContext, useBinaryProtocol: boolean, protocol: Protocol) => void,
): void {
for (const protocol of ['json', 'msgpack'] as const) {
describe(`${title} (${protocol})`, function (this: SuiteContext) {
body.call(this, protocol === 'msgpack', protocol);
});
}
}
Loading
Loading