Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DTP-1076, DTP-1077] Handle isolated create ops and site timeserials vector on the StateObject #1924

Conversation

VeskeR
Copy link
Contributor

@VeskeR VeskeR commented Nov 22, 2024

Removes usage of regionalTimeserial and instead uses siteTimeserials from StateObject to decide which operations to apply (site code parsing will be removed from the SDK side once https://ably.atlassian.net/browse/DTP-1078 is completed on realtime).

Removed created flag from LiveCounter and uses new createOp field on StateObject to handle the initial value for live objects.

The biggest difference from the realtime implementation here is that client SDK merges the initial value from the createOp field (once corresponding op is received via a SYNC sequence or as a separate message) into the data stored on the live object only once (and remembers that it was merged, functions similarly to how created worked for Counters), to avoid merging those values every time a live object is read by client (potentially saves a bunch of computation on merging big Map objects).

Resolves DTP-1076, DTP-1077

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced new methods for creating LiveCounter and LiveMap instances from state objects, enhancing state management.
    • Added a siteCode property to the Timeserial interface for improved site-specific tracking.
    • Added a new helper function for streamlined execution of test scenarios.
  • Bug Fixes

    • Refined handling of state synchronization messages to ensure correct application of operations based on timeserial conditions.
  • Documentation

    • Updated test descriptions for clarity regarding state management and operations.
  • Refactor

    • Streamlined constructors and method signatures across various classes to simplify initialization and improve maintainability.
    • Simplified the handling of state messages and buffered operations in the LiveObjects class.
    • Enhanced the structure and readability of test scenarios related to state operations.

@VeskeR VeskeR requested a review from mschristensen November 22, 2024 08:47
Copy link

coderabbitai bot commented Nov 22, 2024

Walkthrough

The pull request includes significant modifications across multiple classes related to state management in a live objects context. Key changes involve the removal of certain parameters, such as regionalTimeserial, and the introduction of new properties like siteTimeserials and createOp in various state-related classes. The handling of state messages has been streamlined, with a focus on simplifying constructors and method signatures, enhancing clarity and maintainability. Additionally, the test suite has been updated to reflect these changes, ensuring that the new logic is accurately validated.

Changes

File Path Change Summary
src/common/lib/client/realtimechannel.ts Modified processMessage to remove message.channelSerial when calling _liveObjects.handleStateMessages. Adjusted comments and formatting.
src/plugins/liveobjects/livecounter.ts Constructor parameters simplified by removing _created and regionalTimeserial. Added fromStateObject method. Updated applyOperation to remove opRegionalTimeserial. Refined error handling and updated method signatures.
src/plugins/liveobjects/livemap.ts Constructor updated to require objectId only. Introduced fromStateObject method. Removed opRegionalTimeserial from applyOperation. Added liveMapDataFromMapEntries method. Refactored several internal methods.
src/plugins/liveobjects/liveobject.ts Constructor modified to remove optional parameters. Added _siteTimeserials property. Introduced new methods for operation handling and state management. Updated applyOperation method signature.
src/plugins/liveobjects/liveobjects.ts Removed BufferedStateMessage interface. Updated _bufferedStateOperations to use StateMessage. Simplified handleStateMessages method.
src/plugins/liveobjects/liveobjectspool.ts Removed BufferedStateMessage dependency. Simplified createZeroValueObjectIfNotExists method. Restructured applyStateMessages method. Removed applyBufferedStateMessages.
src/plugins/liveobjects/statemessage.ts Removed created property from StateCounter. Updated StateObject to include siteTimeserials and createOp. Introduced new private static methods for encoding and decoding state data.
src/plugins/liveobjects/syncliveobjectsdatapool.ts Updated LiveObjectDataEntry to use stateObject. Removed created and semantics properties from data entry interfaces.
src/plugins/liveobjects/timeserial.ts Added siteCode property to Timeserial interface and DefaultTimeserial class.
test/common/modules/live_objects_helper.js Updated mapObject and counterObject methods to reflect new parameters. Modified return structures to include new properties.
test/realtime/live_objects.test.js Refined handling of STATE_SYNC messages. Updated tests to check for correct application of operations based on timeserials. Added new assertions and refactored test cases for clarity.

Assessment against linked issues

Objective Addressed Explanation
Handle new createOp on state objects (DTP-1076)
Handle site timerserials vector on state objects (DTP-1077)
Use lexico timeserials on state messages and make opaque to client (DTP-1078)

Possibly related PRs

Suggested reviewers

  • mschristensen
  • owenpearson

Poem

In the realm of live objects, we hop and play,
With state messages dancing, in a new ballet.
No more regional tales, just site codes so bright,
Our counters and maps, now shining with light.
With each little change, we grow and we cheer,
For a simpler world, where all is clear! 🐇✨

Warning

Rate limit exceeded

@VeskeR has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 1 minutes and 19 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 059e10b and 245b6a2.

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 eslint (1.23.1)

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

warning eslint@7.32.0: This version is no longer supported. Please see https://eslint.org/version-support for other options.
warning eslint > @humanwhocodes/config-array@0.5.0: Use @eslint/config-array instead
warning eslint > @humanwhocodes/config-array > @humanwhocodes/object-schema@1.2.1: Use @eslint/object-schema instead
warning eslint > file-entry-cache > flat-cache > rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
warning eslint > file-entry-cache > flat-cache > rimraf > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning eslint > file-entry-cache > flat-cache > rimraf > glob > inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
warning grunt > glob@7.1.7: Glob versions prior to v9 are no longer supported
warning grunt > glob > inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
warning grunt > grunt-cli > nopt > osenv@0.1.5: This package is no longer supported.
error eslint-plugin-jsdoc@40.3.0: The engine "node" is incompatible with this module. Expected version "^14 || ^16 || ^17 || ^18 || ^19". Got "22.9.0"
error Found incompatible module.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot temporarily deployed to staging/pull/1924/features November 22, 2024 08:48 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (19)
src/plugins/liveobjects/syncliveobjectsdatapool.ts (1)

21-21: Consider tracking class removal as technical debt.

The TODO comment raises a valid architectural consideration. Since objects are now initialized directly from StateObject, this class's responsibilities have been significantly reduced.

Would you like me to create a GitHub issue to track the potential removal of this class after the createOp changes are fully implemented?

src/plugins/liveobjects/liveobjects.ts (1)

Line range hint 131-136: Address TODO comments for channel state handling.

There are unimplemented TODOs for handling detached/failed and suspended states. These should be addressed to ensure proper cleanup and state management.

Would you like me to help implement the missing channel state handlers or create GitHub issues to track these tasks?

test/common/modules/live_objects_helper.js (2)

181-196: LGTM: Enhanced map object state handling

The changes effectively implement the new state handling approach:

  • Replaced regionalTimeserial with siteTimeserials
  • Separated initial and materialized entries
  • Added conditional createOp generation

Consider adding JSDoc comments to document the purpose of initialEntries vs materialisedEntries parameters, as this distinction is crucial for understanding the new state handling approach.

    mapObject(opts) {
+     /**
+      * @param {Object} opts
+      * @param {string} opts.objectId - The unique identifier for the map object
+      * @param {Object} opts.siteTimeserials - Vector of site-specific timeserials
+      * @param {Object} [opts.initialEntries] - Initial entries used to generate createOp
+      * @param {Object} opts.materialisedEntries - Current materialized state of the map
+      */
      const { objectId, siteTimeserials, initialEntries, materialisedEntries } = opts;

201-215: LGTM: Enhanced counter object state handling

The changes effectively implement the new state handling approach for counters, maintaining consistency with the map object changes:

  • Replaced regionalTimeserial with siteTimeserials
  • Separated initial and materialized counts
  • Added conditional createOp generation

Consider adding JSDoc comments to document the purpose of initialCount vs materialisedCount parameters:

    counterObject(opts) {
+     /**
+      * @param {Object} opts
+      * @param {string} opts.objectId - The unique identifier for the counter object
+      * @param {Object} opts.siteTimeserials - Vector of site-specific timeserials
+      * @param {number} [opts.initialCount] - Initial count used to generate createOp
+      * @param {number} opts.materialisedCount - Current materialized value of the counter
+      */
      const { objectId, siteTimeserials, initialCount, materialisedCount } = opts;
src/common/lib/client/realtimechannel.ts (1)

Line range hint 652-676: Consider adding error logging for state message handling

While the implementation handles state messages correctly, it would be beneficial to add error logging when state messages are received but the LiveObjects plugin is not available, similar to how presence messages are handled.

 if (!this._liveObjects) {
+  Logger.logAction(
+    this.logger,
+    Logger.LOG_ERROR,
+    'RealtimeChannel.processMessage()',
+    'Received state message but LiveObjects plugin is not available'
+  );
   return;
 }
src/plugins/liveobjects/liveobjectspool.ts (1)

90-92: Correct typos in comments for better readability

There are minor typos in the comments that can be corrected to improve clarity.

Suggested changes:

- // we can create a zero-value object for the provided object id, and apply operation for that zero-value object.
+ // we can create a zero-value object for the provided object id and apply the operation to that zero-value object.
- // since they need to ba able to eventually initialize themselves from that *_CREATE op.
+ // since they need to be able to eventually initialize themselves from that *_CREATE op.
src/plugins/liveobjects/liveobject.ts (1)

114-125: Consider Simplifying _timeserialMapFromStringMap Implementation

You can simplify the _timeserialMapFromStringMap method by using Object.fromEntries for better readability and efficiency.

Apply this refactor:

protected _timeserialMapFromStringMap(stringTimeserialsMap: Record<string, string>): Record<string, Timeserial> {
-  const objTimeserialsMap = Object.entries(stringTimeserialsMap).reduce(
-    (acc, v) => {
-      const [key, timeserialString] = v;
-      acc[key] = DefaultTimeserial.calculateTimeserial(this._client, timeserialString);
-      return acc;
-    },
-    {} as Record<string, Timeserial>,
-  );
-
-  return objTimeserialsMap;
+  return Object.fromEntries(
+    Object.entries(stringTimeserialsMap).map(([key, timeserialString]) => [
+      key,
+      DefaultTimeserial.calculateTimeserial(this._client, timeserialString),
+    ]),
+  );
}
src/plugins/liveobjects/livecounter.ts (2)

20-21: Clarify the accessibility of the zeroValue method

The zeroValue static method is marked as @internal in the documentation. If this method is intended for external use, consider updating the documentation to reflect its accessibility. If it should remain internal, ensure it's not exposed publicly.


30-33: Enhance consistency between static methods

The zeroValue and fromStateObject static methods have different parameter orders and purposes. Consider aligning their parameter signatures or providing additional documentation to clarify their usage differences for better maintainability.

test/realtime/live_objects.test.js (10)

Line range hint 489-496: Descriptive Test Case Title

The test case description "can apply MAP_CREATE with primitives state operation messages" is clear and accurately reflects the purpose of the test.

Consider enhancing the test description for clarity:

- description: 'can apply MAP_CREATE with primitives state operation messages',
+ description: 'Should apply MAP_CREATE operations with primitive data types in state operation messages',

623-698: Potential Confusion with Duplicate Serial Numbers

The test injects operations using serial numbers where duplicates may cause confusion:

// Lines 654-659
'bbb@0-0', // existing site, earlier CGO, not applied
'bbb@1-0', // existing site, same CGO, not applied
'bbb@2-0', // existing site, later CGO, applied
'bbb@2-0', // existing site, same CGO (updated from last op), not applied
'aaa@0-0', // different site, earlier CGO, applied
'ccc@9-0', // different site, later CGO, applied

Using the same serial 'bbb@2-0' may cause confusion in understanding which operation is being referred to.

Consider updating serial numbers to be unique to improve clarity:

- 'bbb@2-0', // existing site, same CGO (updated from last op), not applied
+ 'bbb@3-0', // existing site, same CGO (updated from last op), not applied

Adjust comments accordingly to reflect the correct CGO and whether the operation should be applied.


Line range hint 748-756: Typo in Variable Name

In the comments, there is a typo where counter is referred to instead of map:

// Current code
// create new objects and set on root
await liveObjectsHelper.createAndSetOnMap(channelName, {
  mapObjectId: 'root',
  key: 'keyToCounter',
  createOp: liveObjectsHelper.counterCreateOp({ count: 1 }),
});

await liveObjectsHelper.createAndSetOnMap(channelName, {
  mapObjectId: 'root',
  key: 'keyToMap',
  createOp: liveObjectsHelper.mapCreateOp({
    entries: {
      stringKey: { data: { value: 'stringValue' } },
    },
  }),
});

Ensure consistency in comments and variable names:

- // create new objects and set on root
+ // Create new objects and set them on root

// No changes to the code block, but ensure comments correctly describe operations

Additionally, double-check that variable names correspond to the objects they represent to prevent confusion.


1045-1051: Improved Array Initialization

The array of counterIds is manually constructed. This can be simplified using array methods for better readability.

Simplify counterIds initialization:

- const counterIds = [
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
- ];
+ const counterIds = Array.from({ length: 5 }, () => liveObjectsHelper.fakeCounterObjectId());

923-985: Possible Misalignment of Comments and Serial Numbers

In the MAP_REMOVE test case, duplicate serial numbers and comments may cause confusion:

// Lines 952-958
'bbb@0-0', // existing site, earlier site CGO, not applied
'bbb@1-0', // existing site, same site CGO, not applied
'bbb@2-0', // existing site, later site CGO, applied, site timeserial updated
'bbb@2-0', // existing site, same site CGO (updated from last op), not applied
'aaa@0-0', // different site, earlier site CGO, applied
'ccc@9-0', // different site, later site CGO, applied

Consider updating serial numbers for clarity and consistency. Ensure comments accurately reflect whether an operation should be applied based on the updated siteTimeserials.


Line range hint 1366-1485: Complexity in Timeserial Logic Testing

The test case for buffered state operation messages involves complex logic with siteTimeserials. While thorough, the test could benefit from additional comments or splitting into smaller functions for better readability.

Consider refactoring the test to improve readability:

  • Break the test into smaller, focused functions.
  • Add detailed comments explaining each step, especially around the timeserial comparisons.

1045-1051: Typo in Comment

The comment mentions creating a "zero-value counter" using a COUNTER_INC operation, which might be confusing since incrementing would not create a zero value.

Clarify the comment for better understanding:

- // send a COUNTER_INC op first to create a zero-value counter with forged site timeserials vector (from the op), and set it on a root.
+ // Send a COUNTER_INC op to create a counter initialized with a value and set it on the root, using forged site timeserials for testing.

827-834: Clarify Serial Number Comments

Using the same serial number 'bbb@2-0' in consecutive iterations may cause confusion. The comments could be clearer about why operations are or aren't applied.

Update serial numbers and comments for clarity:

'bbb@2-0', // existing site, later CGO, applied
'bbb@3-0', // existing site, same CGO (updated from last op), not applied

Adjust comments to match the serial numbers and explain the reasoning.


1047-1051: Redundant Fake Object ID Generation

Generating fake counter object IDs in a hardcoded array could be replaced with a loop or array method for scalability.

Simplify code with array generation methods:

- const counterIds = [
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
-   liveObjectsHelper.fakeCounterObjectId(),
- ];
+ const counterIds = Array.from({ length: 5 }, () => liveObjectsHelper.fakeCounterObjectId());

827-834: Consistency in Comment Formatting

Ensure that comments consistently describe the operations, expected outcomes, and whether the operation is applied based on the siteTimeserials.

Review comments for accuracy and consistency.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 928dea7 and c8713d1.

📒 Files selected for processing (11)
  • src/common/lib/client/realtimechannel.ts (1 hunks)
  • src/plugins/liveobjects/livecounter.ts (5 hunks)
  • src/plugins/liveobjects/livemap.ts (11 hunks)
  • src/plugins/liveobjects/liveobject.ts (4 hunks)
  • src/plugins/liveobjects/liveobjects.ts (6 hunks)
  • src/plugins/liveobjects/liveobjectspool.ts (3 hunks)
  • src/plugins/liveobjects/statemessage.ts (4 hunks)
  • src/plugins/liveobjects/syncliveobjectsdatapool.ts (2 hunks)
  • src/plugins/liveobjects/timeserial.ts (3 hunks)
  • test/common/modules/live_objects_helper.js (2 hunks)
  • test/realtime/live_objects.test.js (13 hunks)
🧰 Additional context used
📓 Learnings (1)
src/plugins/liveobjects/livecounter.ts (1)
Learnt from: VeskeR
PR: ably/ably-js#1897
File: src/plugins/liveobjects/livecounter.ts:93-93
Timestamp: 2024-11-12T07:31:53.691Z
Learning: In the `LiveCounter` class's `_applyCounterCreate` method, it's intentional to increment the counter's value using `+=` instead of initializing it with `=` because the counter may have a pre-existing non-zero value.
🪛 Biome (1.9.4)
src/plugins/liveobjects/statemessage.ts

[error] 154-154: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 158-158: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 162-162: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 166-166: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 170-170: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 195-195: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 224-224: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 230-230: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 259-259: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 266-266: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🔇 Additional comments (28)
src/plugins/liveobjects/syncliveobjectsdatapool.ts (2)

4-7: LGTM! Clean interface simplification.

The changes effectively streamline the data model by consolidating object data into StateObject, which aligns well with the PR objectives.


83-95: Verify the impact of simplified entry creation.

The entry creation methods have been streamlined to use only StateObject. While this simplification looks good, let's verify that all consumers properly handle this new structure.

✅ Verification successful

Let me gather more information about the usage of these properties in the files identified.


Let me check one more aspect to verify the state object handling in the codebase.


The simplified entry creation is safe and properly integrated

Based on the verification results:

  1. The StateObject interface shows that all necessary properties (counter, map, objectId, etc.) are properly defined and maintained
  2. LiveCounter and LiveMap constructors correctly handle the state objects and their properties
  3. All property accesses in the codebase are properly guarded with null checks (using ?. operator)
  4. The validation logic in overrideWithStateObject methods ensures data consistency

The simplified entry creation aligns well with the existing codebase structure and validation mechanisms.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any direct access to removed properties in the codebase
# This will help ensure all consumers have been updated

# Search for any remaining references to old property names
echo "Checking for potential missed updates to object access patterns..."
rg -l 'objectData|regionalTimeserial|created|semantics' --type ts

# Look for LiveCounter/LiveMap instantiation patterns
echo "Checking LiveCounter/LiveMap usage patterns..."
ast-grep --pattern 'new LiveCounter($$$)' --lang ts
ast-grep --pattern 'new LiveMap($$$)' --lang ts

Length of output: 1147


Script:

#!/bin/bash
# Check the actual usage context of these properties in relevant files
echo "Checking LiveCounter property access patterns..."
rg -A 2 -B 2 'objectData|created|semantics' src/plugins/liveobjects/livecounter.ts

echo "\nChecking LiveMap property access patterns..."
rg -A 2 -B 2 'objectData|created|semantics' src/plugins/liveobjects/livemap.ts

echo "\nChecking state message handling..."
rg -A 2 -B 2 'objectData|created|semantics' src/plugins/liveobjects/statemessage.ts

echo "\nChecking live objects core usage..."
rg -A 2 -B 2 'objectData|created|semantics' src/plugins/liveobjects/liveobjects.ts

Length of output: 2376


Script:

#!/bin/bash
# Check state object handling in the codebase
echo "Checking StateObject usage patterns..."
ast-grep --pattern 'interface StateObject {
  $$$
}' --lang ts

echo "\nChecking state object property access..."
rg -A 3 -B 1 'stateObject\.' src/plugins/liveobjects/

echo "\nChecking LiveCounter and LiveMap initialization..."
rg -A 5 'new (LiveCounter|LiveMap)' src/plugins/liveobjects/

Length of output: 12413

src/plugins/liveobjects/timeserial.ts (3)

12-16: LGTM! Clean interface addition.

The new siteCode property is well-documented and correctly marked as readonly, maintaining the immutable nature of timeserial values.


48-48: LGTM! Property implementation matches interface.

The siteCode property is correctly implemented as public readonly, maintaining consistency with the interface definition.


64-65: Verify the temporary site code parsing implementation.

While this implementation is marked as temporary (pending DTP-1078), there are some considerations:

  1. The hardcoded slice(0, 3) assumes a specific format for seriesId.
  2. There's no validation to ensure seriesId is at least 3 characters long.

Let's verify the seriesId format assumptions:

Consider adding validation or a fallback for cases where seriesId might be shorter than 3 characters, even though this is a temporary solution. This would help prevent potential runtime errors during the transition period.

✅ Verification successful

Let me gather more context about the timeserial format and validation by checking the full implementation.


Temporary site code parsing is safely implemented

The implementation is safe because:

  1. The seriesId is validated before object construction through calculateTimeserial() method, which throws an error for invalid timeserial formats
  2. The zeroValueTimeserial() method uses an empty string for seriesId, which safely results in an empty site code
  3. The site code parsing is only used for comparison operations, where invalid/empty site codes won't cause runtime errors
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for seriesId usage and generation to verify format assumptions
rg -A 3 "seriesId.*=" --type ts

Length of output: 939


Script:

#!/bin/bash
# Get the full TimeSerial class implementation to understand validation and format
cat src/plugins/liveobjects/timeserial.ts

Length of output: 6318

src/plugins/liveobjects/liveobjects.ts (3)

155-157: LGTM! Clear and consistent state message handling.

The simplified approach to applying buffered state operations after SYNC data is clear and maintains proper ordering.


27-27: LGTM! Verify message handling compatibility.

The simplified type for _bufferedStateOperations aligns with the PR objectives to remove regional timeserial tracking.

Let's verify that all state message handlers are compatible with this change:

✅ Verification successful

Type change to _bufferedStateOperations is safe and properly handled

The verification shows that the type change from BufferedStateMessage[] to StateMessage[] is safe and properly implemented:

  • The property is correctly initialized as an empty array in the constructor
  • State messages are buffered during sync via push(...stateMessages)
  • Buffered messages are properly applied using applyStateMessages() and cleared afterward
  • The StateMessage class is properly imported and used throughout the codebase
  • No references to the old BufferedStateMessage type remain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to BufferedStateMessage
rg "BufferedStateMessage" 

# Look for state message handling patterns
ast-grep --pattern 'handleStateMessages($$$)'

Length of output: 73


Script:

#!/bin/bash
# Let's try a broader search for state message handling
rg -A 5 "_bufferedStateOperations" 

# Search for StateMessage type/interface definition
rg "StateMessage(\s+|[{])" 

# Look for handleStateMessages method implementation
ast-grep --pattern 'handleStateMessages($$$) {
  $$$
}'

Length of output: 2936


89-99: Clarify site timeserials handling in state messages.

While the simplified buffering logic looks good, the PR objectives mention adopting siteTimeserials. Could you clarify how site timeserials are being handled in the state messages? This would help ensure the concurrent message handling during SYNC remains correct.

Let's check the StateMessage implementation:

✅ Verification successful

Let me check how the site timeserials are being handled in the state message processing by looking at the LiveObjectsPool implementation:


Let me try a different approach to find the site timeserials handling:


Based on the code analysis, I can now provide a final response:

Site timeserials are properly handled in the implementation

The implementation correctly handles site timeserials through:

  • StateMessage class includes serial field for timeserial format
  • LiveObject base class maintains _siteTimeserials map and implements logic to check if operations can be applied based on site timeserials
  • State messages are buffered during sync and applied afterward, preserving the correct ordering through site timeserials
  • Both LiveMap and LiveCounter implementations properly update site timeserials when applying operations

The simplified buffering mechanism in handleStateMessages works correctly with the site timeserials implementation, as evidenced by the extensive test coverage verifying the behavior.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Look for site timeserials handling in StateMessage
ast-grep --pattern 'class StateMessage {
  $$$
}'

Length of output: 17941


Script:

#!/bin/bash
# Look for applyStateMessages implementation in LiveObjectsPool
ast-grep --pattern 'class LiveObjectsPool {
  $$$
  applyStateMessages($$$) {
    $$$
  }
  $$$
}'

Length of output: 100


Script:

#!/bin/bash
# Search for site timeserials related code
rg -i "sitetimeserial|site.?timeserial" -A 5

Length of output: 19213

test/common/modules/live_objects_helper.js (1)

104-106: LGTM: Map semantics field addition

The addition of the semantics field with value 0 in the map operation structure is consistent with the standardization of map operations.

Let's verify this is used consistently across the codebase:

✅ Verification successful

Semantics field is consistently used in map operations

The verification confirms that:

  • The semantics: 0 field is consistently used in both map creation contexts (lines 105 and 187)
  • The createOp pattern shows this is part of the map object creation flow, used across multiple test scenarios
  • All map creation operations follow the same structure with semantics field
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other map create operations to ensure consistent semantics usage
ast-grep --pattern 'map: {
  semantics: $_
}'

Length of output: 47


Script:

#!/bin/bash
# Let's try a broader search for map semantics usage
rg -n "semantics.*[0-9]" test/common/modules/live_objects_helper.js

# Also search for map creation patterns
rg -n "map:\s*{" -A 3 test/common/modules/live_objects_helper.js

# Search for createOp patterns to understand the context
rg -n "createOp" test/common/modules/live_objects_helper.js

Length of output: 1012

src/common/lib/client/realtimechannel.ts (2)

652-652: Confirm state message handling consistency

The implementation shows a clear distinction between regular state messages and state sync messages, with handleStateMessages and handleStateSyncMessages being called appropriately. The sync version correctly receives the channelSerial parameter while the regular version doesn't, which is consistent with the PR objectives.

Also applies to: 676-676


652-652: Verify removal of channelSerial parameter

The removal of the channelSerial parameter from handleStateMessages aligns with the PR objective to remove regionalTimeserial. However, let's verify that all related code paths have been updated accordingly.

✅ Verification successful

Removal of channelSerial parameter is part of broader refactoring

The changes are part of a larger refactoring that removes regionalTimeserial handling from state message processing. The code changes show:

  • Removal of regionalTimeserial parameter from handleStateMessages
  • Simplified buffering of state messages during sync
  • Updated state object creation to use new fromStateObject factory methods
  • Only one remaining reference to regionalTimeserial in a different context

The changes are consistent and properly implemented across the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining references to channelSerial in state message handling
# across the codebase

# Search for handleStateMessages calls that might still pass channelSerial
rg "handleStateMessages.*channelSerial" 

# Search for any remaining references to regionalTimeserial
rg "regionalTimeserial"

Length of output: 219


Script:

#!/bin/bash
# Let's check the implementation of handleStateMessages and its usage
ast-grep --pattern 'handleStateMessages($_) {
  $$$
}'

# Also check for any references to timeserial in LiveObjects class
rg -A 5 "class LiveObjects" src/plugins/liveobjects/liveobjects.ts

Length of output: 401


Script:

#!/bin/bash
# Let's check the implementation and usage of handleStateMessages with more context
rg -A 10 "handleStateMessages" src/plugins/liveobjects/liveobjects.ts

# Also check for any recent changes to regionalTimeserial
fd -e ts -e js -x grep -l "regionalTimeserial" {} \; | xargs git diff HEAD~5 -- 2>/dev/null

Length of output: 5848

src/plugins/liveobjects/liveobjectspool.ts (3)

57-62: Simplify zero-value object creation

Removing the unnecessary regionalTimeserial parameter from zeroValue methods simplifies object initialization and improves code maintainability.


Line range hint 69-97: Efficient handling of state operations in applyStateMessages

The refactored applyStateMessages method enhances efficiency by creating zero-value objects when they do not exist and applying operations directly. This approach reduces complexity by eliminating the need for buffering operations.


96-96: Ensure non-null assertions are safe

The use of the non-null assertion operator ! assumes that this.get(stateOperation.objectId) will always return a valid object. Confirm that createZeroValueObjectIfNotExists reliably initializes the object to prevent potential runtime errors.

src/plugins/liveobjects/liveobject.ts (2)

137-137: Ensure Subclasses Override applyOperation with Updated Signature

The abstract method applyOperation now includes msg: StateMessage as a parameter. Please verify that all subclasses have updated their implementations of applyOperation to match the new method signature, to prevent runtime errors or TypeScript compilation issues.

Run the following script to verify that all subclasses have updated their applyOperation method:

✅ Verification successful

Let me analyze the implementations more thoroughly by checking the actual method signatures in the subclasses.


Let me try a different approach to get the full context of the implementations.


All Subclasses Correctly Implement Updated Method Signature

The verification shows that all subclasses (LiveMap and LiveCounter) have properly implemented the applyOperation method with the updated signature (op: StateOperation, msg: StateMessage): void, matching the abstract method definition in the parent class.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all implementations of applyOperation to ensure they match the new signature.

# Search for applyOperation method implementations excluding liveobject.ts
rg --type ts --glob '!*liveobject.ts' 'applyOperation\s*\('

Length of output: 547


Script:

#!/bin/bash
# Get the full context of applyOperation implementations
ast-grep --pattern 'applyOperation($_, $_) {
  $$$
}'

Length of output: 55


Script:

#!/bin/bash
# Get more context around the applyOperation implementations
rg --type ts --glob '!*liveobject.ts' -B 2 -A 5 'applyOperation\s*\('

Length of output: 3495


150-150: ⚠️ Potential issue

Implement the New Abstract Method overrideWithStateObject in Subclasses

The abstract method overrideWithStateObject(stateObject: StateObject): TUpdate has been added. Ensure that all subclasses implement this method to comply with the abstract class requirements and to prevent instantiation errors.

Run the following script to identify subclasses lacking the implementation of overrideWithStateObject:

✅ Verification successful

All Subclasses Have Implemented overrideWithStateObject

The verification shows that both direct subclasses of LiveObject (LiveCounter and LiveMap) have properly implemented the overrideWithStateObject method. The SyncLiveObjectsDataPool class, while appearing in the search results, is not a subclass of LiveObject and therefore doesn't need to implement this method.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: List subclasses of LiveObject that do not implement overrideWithStateObject.

# Find subclasses of LiveObject
rg --type ts 'class\s+\w+\s+extends\s+LiveObject' --files-with-matches | while read -r file; do
  if ! rg 'overrideWithStateObject\s*\(' "$file" > /dev/null; then
    echo "$file does not implement overrideWithStateObject"
  fi
done

Length of output: 263


Script:

#!/bin/bash
# Let's check the actual implementations in the subclasses to verify if they have the method
echo "=== LiveCounter implementation ==="
ast-grep --pattern 'class LiveCounter extends LiveObject {
  $$$
  overrideWithStateObject($$$) {
    $$$
  }
  $$$
}' src/plugins/liveobjects/livecounter.ts

echo -e "\n=== LiveMap implementation ==="
ast-grep --pattern 'class LiveMap extends LiveObject {
  $$$
  overrideWithStateObject($$$) {
    $$$
  }
  $$$
}' src/plugins/liveobjects/livemap.ts

# Also verify if there are any other subclasses we might have missed
echo -e "\n=== All LiveObject subclasses ==="
rg -l "extends\s+LiveObject"

Length of output: 744


Script:

#!/bin/bash
# Let's check the content of all files to see the actual implementations
echo "=== LiveCounter.ts content ==="
rg -A 5 -B 5 "class LiveCounter" src/plugins/liveobjects/livecounter.ts

echo -e "\n=== LiveMap.ts content ==="
rg -A 5 -B 5 "class LiveMap" src/plugins/liveobjects/livemap.ts

echo -e "\n=== SyncLiveObjectsDataPool.ts content ==="
rg -A 5 -B 5 "class SyncLiveObjectsDataPool" src/plugins/liveobjects/syncliveobjectsdatapool.ts

# Also check if there are any implementations of overrideWithStateObject in these files
echo -e "\n=== Searching for overrideWithStateObject implementations ==="
rg "overrideWithStateObject.*\{" -A 5 src/plugins/liveobjects/

Length of output: 2718

src/plugins/liveobjects/livecounter.ts (5)

Line range hint 43-61: Approved: Proper handling of operation application and timeserial management

The applyOperation method correctly verifies the objectId, calculates the operation's timeserial, and checks if the operation can be applied based on the site timeserials. The logging of skipped operations due to timeserial checks is appropriately detailed.


145-154: Approved: Intentional summing in _mergeInitialDataFromCreateOperation

The _mergeInitialDataFromCreateOperation method correctly uses += to sum the incoming count from the create operation with the existing data. This approach accommodates scenarios where the counter may already have a non-zero value and aligns with the intended logic.


165-178: Approved: Proper handling of multiple create operations

In _applyCounterCreate, the check for _createOperationIsMerged ensures that duplicate create operations are ignored, preventing inconsistent state. The logging provides clear insight into this decision, aiding in debugging if necessary.


124-134: Confirm the logic in overrideWithStateObject to prevent double-counting

In overrideWithStateObject, _dataRef is initialized with stateObject.counter?.count ?? 0, and then _mergeInitialDataFromCreateOperation adds stateOperation.counter?.count ?? 0 to _dataRef.data. Verify that summing both values is intentional to prevent double-counting the initial counter value.

Run the following script to ensure that the counts are being combined correctly and not counted twice:

✅ Verification successful

The addition of create operation's counter value is intentional and documented

The code explicitly documents that summing the incoming count from the create operation is intentional. From the implementation of _mergeInitialDataFromCreateOperation:

// note that it is intentional to SUM the incoming count from the create op.
// if we got here, it means that current counter instance is missing the initial value in its data reference,
// which we're going to add now.

This is not a double-counting issue, but rather a deliberate design choice where:

  1. First, _dataRef is initialized with the current state (stateObject.counter?.count ?? 0)
  2. Then, if there's a create operation, its value is intentionally added to incorporate the initial value that was missing
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential double-counting in overrideWithStateObject.

# Look for all calls to overrideWithStateObject and analyze related state objects.
rg 'overrideWithStateObject' src/plugins/liveobjects/

# Inspect where stateObject.counter.count and stateObject.createOp.counter.count are set.
rg 'stateObject\.counter\?.count' -A 5 src/plugins/liveobjects/
rg 'stateObject\.createOp\.counter\?.count' -A 5 src/plugins/liveobjects/

Length of output: 1332


Script:

#!/bin/bash
# Let's examine the _mergeInitialDataFromCreateOperation implementation
ast-grep --pattern 'mergeInitialDataFromCreateOperation($_) {
  $$$
}'

# Also check for any direct references to createOp.counter
rg 'createOp.*counter' -A 5 src/plugins/liveobjects/

# Look for any other methods that might modify _dataRef
ast-grep --pattern '_dataRef'

Length of output: 157


Script:

#!/bin/bash
# Let's try a different approach to find the implementation
rg '_mergeInitialDataFromCreateOperation' -A 10 src/plugins/liveobjects/livecounter.ts

# Also search for any methods that modify counter data
rg 'this\._dataRef.*data' -A 5 src/plugins/liveobjects/livecounter.ts

# Look for counter-related operations
rg 'counter\?' -A 5 src/plugins/liveobjects/livecounter.ts

Length of output: 2700


52-64: Verify the existence of siteCode in timeserials

Ensure that opOriginTimeserial.siteCode always exists in this._siteTimeserials before accessing or assigning to prevent potential undefined property access errors.

Run the following script to check for any cases where opOriginTimeserial.siteCode might be undefined:

✅ Verification successful

Let me gather more information about the initialization and usage of _siteTimeserials.


Let me check the initialization method _timeserialMapFromStringMap and the class constructor to understand how _siteTimeserials is initialized.


Let me try a different approach to find the initialization and type information.


Let me check the base class LiveObject to understand how _siteTimeserials is initialized.


Property access is safe, no changes needed

The code is safe because:

  1. _siteTimeserials is initialized as an empty object {} in the base class LiveObject constructor
  2. Before accessing _siteTimeserials[opOriginTimeserial.siteCode], the code calls _canApplyOperation which safely handles the case when the site code doesn't exist
  3. The assignment to _siteTimeserials[opOriginTimeserial.siteCode] is valid as it's adding a new entry to the object if it doesn't exist
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that opOriginTimeserial.siteCode is always defined.

# Search for instantiations of DefaultTimeserial and ensure siteCode is set.
rg -A 3 'DefaultTimeserial\.calculateTimeserial' src/plugins/liveobjects/

# Ensure that all uses of opOriginTimeserial.siteCode are safe.
rg '\.siteCode' src/plugins/liveobjects/livecounter.ts

Length of output: 2927


Script:

#!/bin/bash
# Search for initialization of _siteTimeserials and its type definition
ast-grep --pattern 'class LiveCounter {
  $$$
  private _siteTimeserials: $_ = $_;
  $$$
}'

# Search for any other places where _siteTimeserials is initialized or modified
rg -A 3 '_siteTimeserials' src/plugins/liveobjects/livecounter.ts

Length of output: 778


Script:

#!/bin/bash
# Search for _timeserialMapFromStringMap implementation
ast-grep --pattern 'private _timeserialMapFromStringMap($_) {
  $$$
}'

# Search for LiveCounter constructor
ast-grep --pattern 'class LiveCounter {
  $$$
  constructor($_) {
    $$$
  }
  $$$
}'

# Search for any type definitions related to siteTimeserials
rg -A 3 'interface.*State' src/plugins/liveobjects/livecounter.ts

Length of output: 229


Script:

#!/bin/bash
# Get the entire content of livecounter.ts to analyze initialization and types
cat src/plugins/liveobjects/livecounter.ts

# Also check for any base class that LiveCounter might extend
rg -A 3 'class LiveCounter extends' src/plugins/liveobjects/

Length of output: 7365


Script:

#!/bin/bash
# Get the content of LiveObject class
cat src/plugins/liveobjects/liveobject.ts

# Search for timeserialMapFromStringMap implementation
rg -A 10 '_timeserialMapFromStringMap' src/plugins/liveobjects/

Length of output: 8703

src/plugins/liveobjects/statemessage.ts (2)

107-114: LGTM on adding siteTimeserials and createOp to StateObject

The introduction of siteTimeserials and createOp properties aligns with the PR objectives outlined in DTP-1076 and DTP-1077. The typing and documentation are appropriate and accurately reflect the intended functionality.


244-244: Avoid using JSON.parse(JSON.stringify(...)) for deep copying when buffers are involved

This issue also applies to the deep copy of stateObject. Ensure that buffers are correctly copied by using a suitable deep copy method that handles Buffer and Uint8Array types.

test/realtime/live_objects.test.js (5)

113-113: Consistency in Handling State Messages

The processStateObjectMessageOnChannel method correctly updates to use siteTimeserials instead of regionalTimeserial. This change aligns with the PR objectives to remove regionalTimeserial and adopt siteTimeserials.


280-281: Proper Initialization of siteTimeserials and initialEntries

The siteTimeserials and initialEntries are correctly set on the mapObject, ensuring that the state object is properly initialized during tests.


1214-1222: Asynchronous Test Execution

In the loop over applyOperationsScenarios, ensure that all asynchronous operations are properly awaited to prevent unpredictable test execution.

Check that all promises in the test cases are correctly awaited.


923-986: Potential Shadowing of Variables

In the function scope, mapId is declared and used. Ensure that variable names are not shadowing or conflicting with other variables in the broader scope.

Check for variable shadowing:

Verify that mapId is not declared elsewhere in a conflicting scope.


1047-1051: ⚠️ Potential issue

Ensure Unique Object IDs

When generating fake counter object IDs, it's important that they are unique to prevent collisions in tests.

Confirm that fakeCounterObjectId() produces unique IDs. If not, modify the implementation to guarantee uniqueness.

Run the following script to check for uniqueness:

Expect no output, indicating all IDs are unique.

src/plugins/liveobjects/statemessage.ts Outdated Show resolved Hide resolved
src/plugins/liveobjects/statemessage.ts Outdated Show resolved Hide resolved
src/plugins/liveobjects/livemap.ts Show resolved Hide resolved
src/plugins/liveobjects/livemap.ts Show resolved Hide resolved
@VeskeR VeskeR force-pushed the liveobjects/isolated-create-ops-and-vector-timeserials branch from c8713d1 to e151919 Compare November 22, 2024 08:55
@github-actions github-actions bot temporarily deployed to staging/pull/1924/bundle-report November 22, 2024 08:56 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1924/features November 22, 2024 08:56 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1924/typedoc November 22, 2024 08:56 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (13)
src/plugins/liveobjects/syncliveobjectsdatapool.ts (2)

21-22: Consider creating a tracking issue for class evaluation.

The TODO comment raises a valid architectural concern about the continued necessity of this class. Since this evaluation is outside the scope of the current PR objectives (DTP-1076, DTP-1077), it would be beneficial to track this separately.

Would you like me to help create a GitHub issue to track the evaluation of whether this class can be removed or simplified further?


Line range hint 50-80: Consider enhancing error logging details.

While the error handling is good, the log messages could be more helpful for debugging by including the actual values received.

-          `received unsupported state object message during SYNC, expected 'counter' or 'map' to be present, skipping message; message id: ${stateMessage.id}, channel: ${this._channel.name}`,
+          `received unsupported state object message during SYNC, expected 'counter' or 'map' to be present, skipping message; message id: ${stateMessage.id}, channel: ${this._channel.name}, received keys: ${Object.keys(stateObject).join(', ')}`,
src/plugins/liveobjects/liveobjectspool.ts (1)

90-94: Well-documented architectural change for operation handling.

The new approach of creating zero-value objects and allowing them to handle their own create operations is a clean architectural solution. The comments effectively explain the rationale behind this design decision.

Consider adding error logging for cases where operation application fails, to help with debugging in production.

src/plugins/liveobjects/liveobject.ts (2)

103-125: Consider adding error handling for invalid timeserial strings.

The new methods look good, but _timeserialMapFromStringMap might benefit from error handling for invalid timeserial strings to prevent runtime errors.

 protected _timeserialMapFromStringMap(stringTimeserialsMap: Record<string, string>): Record<string, Timeserial> {
   const objTimeserialsMap = Object.entries(stringTimeserialsMap).reduce(
     (acc, v) => {
       const [key, timeserialString] = v;
+      try {
         acc[key] = DefaultTimeserial.calculateTimeserial(this._client, timeserialString);
+      } catch (error) {
+        this._client.logger.warn(`Invalid timeserial string: ${timeserialString}`, error);
+        // Skip invalid timeserial
+      }
       return acc;
     },
     {} as Record<string, Timeserial>,
   );

   return objTimeserialsMap;
 }

137-166: LGTM! Well-documented state handling changes.

The abstract method changes are well-documented and align perfectly with the PR objectives. The performance optimization strategy of merging initial data only once is a good architectural decision.

A few architectural considerations to keep in mind:

  1. Ensure implementing classes maintain the same optimization guarantees
  2. Consider adding metrics to track the performance improvement
  3. Document the state transition diagram for future maintainers
src/plugins/liveobjects/livecounter.ts (2)

96-134: Consider refactoring validation logic for better maintainability

The validation logic contains repeated patterns and similar error messages. Consider extracting the validation into a separate method to improve maintainability.

+ private _validateStateObject(stateObject: StateObject): void {
+   const validateId = (id: string, context: string) => {
+     if (id !== this.getObjectId()) {
+       throw new this._client.ErrorInfo(
+         `Invalid state object: ${context} objectId=${id}; LiveCounter objectId=${this.getObjectId()}`,
+         50000,
+         500
+       );
+     }
+   };
+
+   validateId(stateObject.objectId, "state object");
+
+   if (!this._client.Utils.isNil(stateObject.createOp)) {
+     validateId(stateObject.createOp.objectId, "state object createOp");
+
+     if (stateObject.createOp.action !== StateOperationAction.COUNTER_CREATE) {
+       throw new this._client.ErrorInfo(
+         `Invalid state object: state object createOp action=${stateObject.createOp?.action}; LiveCounter objectId=${this.getObjectId()}`,
+         50000,
+         500
+       );
+     }
+   }
+ }

164-178: Consider clarifying the comment about object ID encoding

The comment about object ID encoding the operation could be more explicit about why this guarantees uniqueness of create operations.

-    // There can't be two different create operation for the same object id, because the object id
-    // fully encodes that operation. This means we can safely ignore any new incoming create operations
-    // if we already merged it once.
+    // There can't be two different create operations for the same object id because:
+    // 1. The object id is derived from the create operation's unique properties
+    // 2. Any create operation with the same object id must be identical to the original
+    // Therefore, we can safely ignore subsequent create operations once the initial one is merged.
src/plugins/liveobjects/liveobjects.ts (1)

155-157: Enhance comment clarity regarding operation order

The implementation correctly applies buffered operations after SYNC data. Consider enhancing the comment to explicitly mention why this order is important:

-    // should apply buffered state operations after we applied the SYNC data.
-    // can use regular state messages application logic
+    // Apply buffered state operations after SYNC data to ensure:
+    // 1. Base state is established from SYNC
+    // 2. Any concurrent updates are applied in order
+    // Using regular state messages application logic for consistency
test/common/modules/live_objects_helper.js (1)

181-196: LGTM! Consider adding parameter validation

The changes effectively separate initial and materialised state, aligning with the PR objectives. The code could benefit from parameter validation.

Consider adding parameter validation:

 mapObject(opts) {
   const { objectId, siteTimeserials, initialEntries, materialisedEntries } = opts;
+  if (!objectId) {
+    throw new Error('objectId is required');
+  }
+  if (!siteTimeserials) {
+    throw new Error('siteTimeserials is required');
+  }
   const obj = {
src/plugins/liveobjects/livemap.ts (1)

360-366: Improve error message handling for undefined map

The error message uses optional chaining which could result in "undefined" being displayed in the error message.

Apply this diff to improve error handling:

-    if (this._semantics !== op.map?.semantics) {
+    if (!op.map) {
+      throw new this._client.ErrorInfo(
+        `Cannot apply MAP_CREATE op on LiveMap objectId=${this.getObjectId()}: map is undefined`,
+        50000,
+        500,
+      );
+    }
+    if (this._semantics !== op.map.semantics) {
       throw new this._client.ErrorInfo(
-        `Cannot apply MAP_CREATE op on LiveMap objectId=${this.getObjectId()}; map's semantics=${this._semantics}, but op expected ${op.map?.semantics}`,
+        `Cannot apply MAP_CREATE op on LiveMap objectId=${this.getObjectId()}; map's semantics=${this._semantics}, but op expected ${op.map.semantics}`,
         50000,
         500,
       );
     }
test/realtime/live_objects.test.js (3)

623-698: Add documentation for site timeserials vector rules.

The test cases for site timeserials vector handling would benefit from additional documentation explaining:

  • The format and components of timeserials (e.g., 'bbb@1-0')
  • The rules for when operations are applied or rejected
  • The significance of different site codes (aaa, bbb, ccc)

This would make the test cases more maintainable and easier to understand.

Add this documentation above the test:

/**
 * Timeserial format: <siteCode>@<cgo>-<serial>
 * where:
 * - siteCode: unique identifier for the site (e.g., 'aaa', 'bbb')
 * - cgo: causally-globally-ordered sequence number
 * - serial: operation sequence number
 * 
 * Rules for applying operations:
 * 1. For existing site: apply if CGO is later than current
 * 2. For different site: apply if no conflicting operation exists
 * 3. Operations are buffered during sync and applied based on these rules
 */

Line range hint 1366-1480: Improve code readability with constants and descriptive names.

The test code could be more maintainable with these improvements:

  • Define constants for array indices
  • Use more descriptive variable names
  • Group related test cases

Consider this refactoring:

-  const mapId = liveObjectsHelper.fakeMapObjectId();
-  const counterId = liveObjectsHelper.fakeCounterObjectId();
+  const TEST_MAP_ID = liveObjectsHelper.fakeMapObjectId();
+  const TEST_COUNTER_ID = liveObjectsHelper.fakeCounterObjectId();

+  // Operation types for clarity
+  const OP_TYPES = {
+    EARLIER_SITE_CGO: 'earlier site CGO',
+    SAME_SITE_CGO: 'same site CGO',
+    LATER_SITE_CGO: 'later site CGO',
+    DIFFERENT_SITE: 'different site'
+  };

+  const testCases = [
+    { serial: 'bbb@1-0', type: OP_TYPES.EARLIER_SITE_CGO },
+    { serial: 'bbb@2-0', type: OP_TYPES.SAME_SITE_CGO },
+    // ... other cases
+  ];

-  for (const [i, serial] of [...].entries())
+  for (const testCase of testCases)

1214-1220: Consider reorganizing test scenarios for better maintainability.

The test scenarios could be better organized by:

  • Grouping related scenarios into separate describe blocks
  • Adding consistent error handling across all scenarios
  • Using shared setup functions for common test patterns

Consider this structure:

describe('Operation Application', () => {
  beforeEach(setupTestEnvironment);
  
  describe('During Sync', () => {
    // Sync-related scenarios
  });
  
  describe('Site Timeserials', () => {
    // Timeserial-related scenarios
  });
});

function setupTestEnvironment() {
  // Common setup code
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between c8713d1 and e151919.

📒 Files selected for processing (10)
  • src/common/lib/client/realtimechannel.ts (1 hunks)
  • src/plugins/liveobjects/livecounter.ts (5 hunks)
  • src/plugins/liveobjects/livemap.ts (11 hunks)
  • src/plugins/liveobjects/liveobject.ts (4 hunks)
  • src/plugins/liveobjects/liveobjects.ts (6 hunks)
  • src/plugins/liveobjects/liveobjectspool.ts (3 hunks)
  • src/plugins/liveobjects/statemessage.ts (4 hunks)
  • src/plugins/liveobjects/syncliveobjectsdatapool.ts (2 hunks)
  • test/common/modules/live_objects_helper.js (2 hunks)
  • test/realtime/live_objects.test.js (13 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/common/lib/client/realtimechannel.ts
🧰 Additional context used
📓 Learnings (2)
src/plugins/liveobjects/livecounter.ts (1)
Learnt from: VeskeR
PR: ably/ably-js#1897
File: src/plugins/liveobjects/livecounter.ts:93-93
Timestamp: 2024-11-12T07:31:53.691Z
Learning: In the `LiveCounter` class's `_applyCounterCreate` method, it's intentional to increment the counter's value using `+=` instead of initializing it with `=` because the counter may have a pre-existing non-zero value.
src/plugins/liveobjects/syncliveobjectsdatapool.ts (1)
Learnt from: VeskeR
PR: ably/ably-js#1897
File: src/plugins/liveobjects/syncliveobjectsdatapool.ts:57-57
Timestamp: 2024-11-12T07:31:53.691Z
Learning: When methods with the same name exist in different classes, references to these methods may remain valid even if a similar method is renamed in another class.
🪛 Biome (1.9.4)
src/plugins/liveobjects/statemessage.ts

[error] 154-154: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 158-158: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 162-162: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 166-166: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 170-170: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 195-195: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 224-224: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 230-230: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 259-259: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 266-266: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🔇 Additional comments (22)
src/plugins/liveobjects/syncliveobjectsdatapool.ts (2)

4-18: LGTM! Clean interface refactoring.

The simplified imports and interface updates align well with the PR objectives. The transition to using StateObject directly in the interfaces provides a more cohesive data model.


82-95: Verify method signature changes across the codebase.

The simplified method signatures look good, but let's ensure all callers have been updated to handle the new parameter list.

✅ Verification successful

Method signature changes are correctly implemented

The verification shows that both methods are only called internally within the SyncLiveObjectsDataPool class, and all calls have been updated to use the simplified signature with just the stateObject parameter. The changes are consistent throughout the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any calls to these methods to verify they've been updated
ast-grep --pattern $'_createLiveCounterDataEntry($$$)' 
ast-grep --pattern $'_createLiveMapDataEntry($$$)'

Length of output: 107


Script:

#!/bin/bash
# Search for method calls with ripgrep for better pattern matching
rg "_createLiveCounterDataEntry" -A 2
rg "_createLiveMapDataEntry" -A 2

# Also search for the class name to understand the context
rg "class.*SyncLiveObjectsDataPool" -A 5

Length of output: 1894

src/plugins/liveobjects/liveobjectspool.ts (3)

6-6: LGTM! Import changes align with simplified state handling.

The removal of BufferedStateMessage and DefaultTimeserial imports, while adding direct StateMessage imports, reflects the architectural shift towards simplified state handling.

Also applies to: 8-8


57-57: Verify the removal of regionalTimeserial parameter.

The removal of regionalTimeserial from zero value creation aligns with the PR objectives. However, let's ensure this change doesn't affect existing functionality.

Also applies to: 62-62

✅ Verification successful

Based on the comprehensive test file analysis, I can now provide a definitive response about the removal of regionalTimeserial parameter.

Removal of regionalTimeserial parameter is safe and aligns with the codebase

The test file confirms that the zero value creation functionality works without regionalTimeserial parameter, and there are no existing dependencies on this parameter. The tests demonstrate that:

  1. Zero value objects are created and managed correctly without regionalTimeserial
  2. The state synchronization and operations work properly with the simplified object creation
  3. All test cases pass without requiring regionalTimeserial parameter
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to regionalTimeserial to ensure complete removal
rg -i "regionalTimeserial" 

# Search for tests covering zero value creation
rg -l "zeroValue.*test" 

Length of output: 52


Script:

#!/bin/bash
# Let's search for LiveMap.zeroValue and LiveCounter.zeroValue usage patterns
ast-grep --pattern 'LiveMap.zeroValue($$$)'
ast-grep --pattern 'LiveCounter.zeroValue($$$)'

# Let's also check the implementation of these methods
rg -A 5 "zeroValue.*=.*function" 
rg -A 5 "zeroValue.*=.*\("

# Look for test files related to LiveMap and LiveCounter
fd -e ts -e js "live(map|counter).*test"

Length of output: 1302


Script:

#!/bin/bash
# Let's check the implementation of LiveMap and LiveCounter classes
ast-grep --pattern 'class LiveMap {
  $$$
  static zeroValue($$$) {
    $$$
  }
  $$$
}'

ast-grep --pattern 'class LiveCounter {
  $$$
  static zeroValue($$$) {
    $$$
  }
  $$$
}'

# Let's also look for any tests that might cover these classes
fd "live" -e test.ts -e test.js

Length of output: 252


Script:

#!/bin/bash
# Let's examine the test file to see how zero values are tested
cat test/realtime/live_objects.test.js

# Let's also check for any historical usage of regionalTimeserial in the git history
git log -S "regionalTimeserial" --oneline

Length of output: 87598


96-96: Verify handling of race conditions in operation application.

The direct application of operations to zero-value objects could potentially lead to race conditions if operations arrive out of order. Please ensure that:

  1. Create operations are properly handled even if they arrive after other operations
  2. The state remains consistent when operations are applied to zero-value objects
✅ Verification successful

Race conditions in operation application are properly handled

The codebase has robust mechanisms to handle operation ordering and race conditions:

  1. Operations are ordered using a vector clock system with timeserials (timestamp + site code + counter), ensuring consistent ordering across distributed clients
  2. Each object maintains a site timeserials vector to track the last applied operation from each site
  3. Operations are only applied if their timeserial is strictly greater than the stored timeserial for that site
  4. During sync, operations are buffered and applied in order after the sync completes
  5. Zero-value objects handle operations correctly through the timeserial comparison mechanism

The implementation ensures that:

  • Create operations arriving late are handled correctly through timeserial comparison
  • Operations cannot be applied out of order due to the vector clock system
  • State remains consistent through proper operation ordering and buffering

The code shows a well-designed distributed state management system that properly handles concurrent operations and race conditions.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for test cases covering operation ordering
rg -A 5 "test.*operation.*order" 

# Look for error handling in operation application
ast-grep --pattern 'applyOperation($_) {
  try {
    $$$
  } catch {
    $$$
  }
}'

Length of output: 120


Script:

#!/bin/bash
# Search for applyOperation implementation to understand operation handling
ast-grep --pattern 'applyOperation($params) {
  $$$
}'

# Look for operation ordering or sequence handling
rg -l "operation.*sequence|sequence.*operation|order.*operation"

# Search for create operation handling
rg -A 5 "case\s+['\"](create|CREATE)['\"]"

# Check LiveObjectsPool implementation for operation handling
rg -A 10 "class LiveObjectsPool"

Length of output: 1056


Script:

#!/bin/bash
# Check test file for operation ordering tests
cat test/realtime/live_objects.test.js

# Look for LiveObject class implementation to understand operation handling
ast-grep --pattern 'class LiveObject {
  $$$
}'

# Search for sequence number or timestamp handling in operations
rg -A 5 "sequence.*number|timestamp|orderBy"

Length of output: 105380


Script:

#!/bin/bash
# Search for operation ordering in LiveObjectsPool
rg -A 10 "applyOperation|processStateOperation" src/plugins/liveobjects/liveobjectspool.ts

# Look for state sync and operation buffering
rg -A 10 "STATE_SYNC|_buffer" src/plugins/liveobjects/

# Check for timeserial or vector clock handling
rg -A 5 "timeserial|siteTimeserials|vector" src/plugins/liveobjects/

Length of output: 29652

src/plugins/liveobjects/liveobject.ts (3)

44-54: LGTM! Constructor changes align with objectives.

The simplified constructor with proper initialization of new fields and clear documentation of the empty timeserials vector purpose looks good.


Line range hint 90-102: LGTM! Good optimization for update notifications.

The noop check prevents unnecessary event emissions, which is a good optimization.


4-4: LGTM! Verify StateObject usage across codebase.

The new imports and properties align well with the PR objectives. The _dataRef documentation clearly explains its purpose in combining initial and state operations.

Also applies to: 40-42

✅ Verification successful

StateObject import and usage is consistent across the codebase

The StateObject interface is properly defined in statemessage.ts and is consistently used across the live objects implementation:

  • Used as a type in LiveObjectDataEntry for state management
  • Imported and referenced in all relevant live object files (LiveCounter, LiveMap, LiveObject)
  • Interface properly defines required properties (objectId, siteTimeserials)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify StateObject usage across the codebase
ast-grep --pattern 'StateObject' 

Length of output: 469


Script:

#!/bin/bash
# Check how StateObject is used in the codebase
ast-grep --pattern 'StateObject' -A 10

Length of output: 2955


Script:

#!/bin/bash
# Check StateObject type definition
rg "interface StateObject" -A 5

Length of output: 538


Script:

#!/bin/bash
# Check actual usage of StateObject in implementations
rg "implements StateObject" -A 5

Length of output: 34

src/plugins/liveobjects/livecounter.ts (3)

20-33: LGTM: Clean implementation of factory methods

The simplified zeroValue and new fromStateObject static methods provide clear and type-safe ways to instantiate LiveCounter objects. The implementation aligns well with the PR objectives of streamlining state object handling.


145-154: LGTM: Correct implementation of initial data merging

The implementation correctly uses += for merging the initial counter value, which aligns with the previous learning that counters may have pre-existing non-zero values.


52-64: Verify the timing of site timeserial updates

The site timeserial is updated before the operation is actually processed. While the comment indicates this is intentional, it's worth verifying that this doesn't lead to missed operations in case of subsequent failures.

✅ Verification successful

Let me gather more context about the operation processing and error handling.


Let me try a different approach to understand the operation handling flow.


Based on the gathered evidence, I can now generate the final response:

Early timeserial update is safe and intentional

The early update of site timeserial before operation processing is correct and safe because:

  • All operation failures are handled through explicit throws that will prevent any subsequent operations
  • The increment operation (_applyCounterInc) is a simple atomic operation that cannot fail
  • The create operation (_applyCounterCreate) either succeeds or returns a no-op
  • The comment accurately reflects the design intent - marking operations as processed is independent of their success
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any error handling or rollback mechanisms around timeserial updates
rg -A 5 "_siteTimeserials\[.*\]\s*=" 

Length of output: 985


Script:

#!/bin/bash
# Search for operation application methods and their error handling
ast-grep --pattern 'private _apply$_($$$) {
  $$$
}'

# Also check the specific operation handlers
ast-grep --pattern 'private _applyCounter$_($$$) {
  $$$
}'

Length of output: 116


Script:

#!/bin/bash
# Look for operation handling methods and their implementations
rg -A 10 "_applyCounter" src/plugins/liveobjects/

# Check for any error handling patterns
rg -B 2 -A 5 "throw|catch|error" src/plugins/liveobjects/livecounter.ts

Length of output: 5506

src/plugins/liveobjects/liveobjects.ts (4)

27-27: LGTM: Type simplification aligns with objectives

The change from BufferedStateMessage[] to StateMessage[] simplifies the state message handling, which aligns well with the PR objectives.


198-200: LGTM: Batch notifications improve performance

The implementation correctly batches notifications to ensure clients are notified only once after all changes are applied.


210-214: Verify factory method implementations

The switch to factory methods aligns with the objective to handle the new createOp field.

Let's verify the factory method implementations:

✅ Verification successful

Factory methods correctly handle state object initialization

The verification confirms that both LiveCounter and LiveMap have proper fromStateObject factory methods that:

  1. Create new instances with required parameters from the state object
  2. Apply state using overrideWithStateObject
  3. Handle the state object interface which includes the new createOp field

The implementation aligns with the objective to support the createOp field in the StateObject interface.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check LiveCounter and LiveMap factory method implementations
ast-grep --pattern 'static fromStateObject($_: $_) {
  $$$
}'

# Check for createOp handling in the factory methods
rg -A 5 "createOp.*fromStateObject" || rg -A 5 "fromStateObject.*createOp"

Length of output: 139


Script:

#!/bin/bash
# Let's search for LiveCounter and LiveMap class implementations
ast-grep --pattern 'class LiveCounter {
  $$$
}'

ast-grep --pattern 'class LiveMap {
  $$$
}'

# Also search for any fromStateObject method mentions
rg "fromStateObject" -A 5

Length of output: 1770


Script:

#!/bin/bash
# Let's check the overrideWithStateObject implementation in both classes
ast-grep --pattern 'overrideWithStateObject($$$) {
  $$$
}'

# Also check StateObject type definition
rg "type StateObject" -A 10 || rg "interface StateObject" -A 10

Length of output: 1004


89-99: Verify concurrent message handling during SYNC

The simplified state message handling looks good and aligns with the objective to remove regional timeserial. The buffering logic for concurrent messages during SYNC is clearer.

Let's verify the concurrent message handling implementation across the codebase:

✅ Verification successful

Based on the gathered evidence, I can now provide the final response:

Concurrent message handling during SYNC is properly implemented

The implementation correctly handles concurrent state messages during SYNC by:

  • Buffering incoming messages in _bufferedStateOperations while sync is in progress
  • Clearing the buffer at the start of a new sync
  • Applying all buffered messages after sync completion
  • Emitting a SyncCompleted event that clients can await

The code follows a clear and consistent pattern for managing state synchronization, with proper cleanup and event handling.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for tests that verify concurrent message handling during SYNC
rg -A 5 "test.*concurrent.*sync" || rg -A 5 "describe.*concurrent.*sync"

# Search for related error handling
rg "handleStateMessages.*error" || rg "error.*handleStateMessages"

Length of output: 141


Script:

#!/bin/bash
# Let's search for tests related to state message handling and sync
rg -A 10 "describe.*LiveObjects.*state.*messages?" || rg -A 10 "test.*state.*messages?"

# Search for SYNC related implementations
ast-grep --pattern 'handleStateMessages($$$) { $$$_syncInProgress$$$ }'

# Look for buffered state operations handling
rg -A 5 "_bufferedStateOperations" 

Length of output: 3472


Script:

#!/bin/bash
# Let's check the LiveObjectsPool implementation for state message handling
ast-grep --pattern 'class LiveObjectsPool { $$$ applyStateMessages($$$) { $$$ } $$$ }'

# Look for sync completion handling
rg -A 10 "SyncCompleted" 

# Check for any error handling in state message application
rg -A 5 "applyStateMessages.*catch" || rg -A 5 "try.*applyStateMessages"

Length of output: 2588

test/common/modules/live_objects_helper.js (2)

201-215: LGTM! Consistent implementation with mapObject

The changes mirror the mapObject pattern, correctly handling both initial and materialised states. The null check for initialCount is appropriate as it allows 0 as a valid count.


104-106: Verify the semantics field usage across the codebase

The addition of semantics: 0 in the map creation operation appears to be a new versioning or type identifier. Let's verify its usage and handling.

✅ Verification successful

The semantics field is correctly used for LiveMap versioning

The semantics field is properly integrated into the LiveMap implementation:

  • It's consistently initialized to 0 in test helpers
  • LiveMap validates semantics field matches during state object loading and operation application
  • Mismatched semantics trigger appropriate error messages
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other occurrences of map semantics field to ensure consistent usage
rg -g '!*.{json,lock}' 'semantics.*:.*0' 
rg -g '!*.{json,lock}' 'map.*semantics'

Length of output: 1166

src/plugins/liveobjects/statemessage.ts (2)

107-115: LGTM! Well-documented interface changes

The addition of siteTimeserials and createOp to the StateObject interface is well-structured and thoroughly documented. The changes align perfectly with the PR objectives of handling site timeserials and isolated create operations.


320-331: LGTM! Clean implementation of JSON encoding

The changes to toJSON() properly handle the encoding of operations and objects, with clear separation between base64 and binary encoding paths.

src/plugins/liveobjects/livemap.ts (3)

132-144: LGTM! Good improvements to operation handling

The changes improve the operation handling by:

  1. Correctly updating site timeserials before processing the operation
  2. Adding detailed logging for skipped operations
  3. Properly calculating operation origin timeserial

461-468: Add entry.data validation

The method accesses entry.data properties without validation, which could cause runtime errors if entry.data is undefined. This matches a previous review comment that's still valid.


Line range hint 1-484: Overall implementation looks good!

The changes successfully implement the PR objectives:

  1. Proper handling of isolated create operations
  2. Correct implementation of site timeserials vector
  3. Thorough validation of state objects

The core functionality is solid, though there are some safety improvements needed around optional chaining and non-null assertions as noted in the specific comments.

src/plugins/liveobjects/statemessage.ts Show resolved Hide resolved
src/plugins/liveobjects/statemessage.ts Outdated Show resolved Hide resolved
src/plugins/liveobjects/statemessage.ts Outdated Show resolved Hide resolved
src/plugins/liveobjects/livemap.ts Show resolved Hide resolved
src/plugins/liveobjects/livemap.ts Show resolved Hide resolved
Copy link
Contributor

@mschristensen mschristensen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks Andrii

src/plugins/liveobjects/liveobjectspool.ts Outdated Show resolved Hide resolved
src/plugins/liveobjects/statemessage.ts Outdated Show resolved Hide resolved
src/plugins/liveobjects/livecounter.ts Show resolved Hide resolved
}

export type AnyDataEntry = LiveCounterDataEntry | LiveMapDataEntry;

// TODO: investigate if this class is still needed after changes with createOp. objects are now initialized from the stateObject and this class does minimal processing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the copy of the pool for sync just be a LiveObjectsPool instance? Maybe not, in order to preserve data refs?

Copy link
Contributor Author

@VeskeR VeskeR Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not even a copy of anything at this point, SyncLiveObjectsDataPool just buffers incoming state objects during sync and that's all.
the caller class, LiveObjects, then just gets the full array of buffered state objects and "applies the sync" by actully creating new objects/overriding existing ones.

I'll keep this SyncLiveObjectsDataPool class around for a bit, until all api is in and we're a bit closer to the stable protocol. If at that point it still does as little as it does in this PR, I'll just move buffering to the caller LiveObjects class (it already does buffering of state ops, so seems reasonable to also buffer state objects).

@VeskeR VeskeR force-pushed the liveobjects/isolated-create-ops-and-vector-timeserials branch from e151919 to 567ad15 Compare November 22, 2024 17:09
@github-actions github-actions bot temporarily deployed to staging/pull/1924/bundle-report November 22, 2024 17:10 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1924/features November 22, 2024 17:10 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1924/typedoc November 22, 2024 17:10 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (5)
src/plugins/liveobjects/liveobject.ts (1)

114-125: Consider error handling for invalid timeserial strings.

The _timeserialMapFromStringMap method should handle potential invalid timeserial strings gracefully.

 protected _timeserialMapFromStringMap(stringTimeserialsMap: Record<string, string>): Record<string, Timeserial> {
   const objTimeserialsMap = Object.entries(stringTimeserialsMap).reduce(
     (acc, v) => {
       const [key, timeserialString] = v;
+      try {
         acc[key] = DefaultTimeserial.calculateTimeserial(this._client, timeserialString);
+      } catch (error) {
+        this._client.logger.warn(`Invalid timeserial string: ${timeserialString}`, error);
+      }
       return acc;
     },
     {} as Record<string, Timeserial>,
   );

   return objTimeserialsMap;
 }
src/plugins/liveobjects/liveobjects.ts (1)

Line range hint 198-214: Consider adding error handling for state object operations.

While the implementation looks good, consider adding error handling for potential issues during state object operations:

  1. State object validation before override
  2. Error handling in factory methods
  3. Validation of update objects before notification

Example error handling:

const update = existingObject.overrideWithStateObject(entry.stateObject);
if (!update) {
  this._client.Logger.logAction(
    this._client.logger,
    this._client.Logger.LOG_ERROR,
    'LiveObjects._applySync()',
    `Failed to override state for object ${objectId}`
  );
  continue;
}
src/plugins/liveobjects/livemap.ts (2)

186-240: Consider extracting validation logic into separate methods

The validation logic in overrideWithStateObject is complex and could be made more maintainable by extracting it into separate validation methods.

Consider refactoring like this:

+  private _validateStateObjectId(stateObject: StateObject): void {
+    if (stateObject.objectId !== this.getObjectId()) {
+      throw new this._client.ErrorInfo(
+        `Invalid state object: state object objectId=${stateObject.objectId}; LiveMap objectId=${this.getObjectId()}`,
+        50000,
+        500,
+      );
+    }
+  }
+
+  private _validateMapSemantics(map: StateObject['map']): void {
+    if (!map || map.semantics !== this._semantics) {
+      throw new this._client.ErrorInfo(
+        `Invalid state object: state object map semantics=${map?.semantics}; LiveMap semantics=${this._semantics}`,
+        50000,
+        500,
+      );
+    }
+  }
+
+  private _validateCreateOp(createOp: StateOperation): void {
+    if (createOp.objectId !== this.getObjectId()) {
+      throw new this._client.ErrorInfo(
+        `Invalid state object: state object createOp objectId=${createOp.objectId}; LiveMap objectId=${this.getObjectId()}`,
+        50000,
+        500,
+      );
+    }
+    // ... rest of create op validation
+  }

   overrideWithStateObject(stateObject: StateObject): LiveMapUpdate {
-    if (stateObject.objectId !== this.getObjectId()) {
-      // ... validation logic
-    }
-    if (stateObject.map?.semantics !== this._semantics) {
-      // ... validation logic
-    }
+    this._validateStateObjectId(stateObject);
+    this._validateMapSemantics(stateObject.map);
     
     if (!this._client.Utils.isNil(stateObject.createOp)) {
-      // ... validation logic
+      this._validateCreateOp(stateObject.createOp);
     }
     
     // ... rest of the method
   }

346-369: Consider adding debug logging for create operation merging

The create operation merging logic would benefit from additional debug logging to help diagnose issues in production.

Add logging for successful merges:

   private _applyMapCreate(op: StateOperation): LiveMapUpdate | LiveObjectUpdateNoop {
     if (this._createOperationIsMerged) {
       // ... existing logging for skipped merges
       return { noop: true };
     }

     if (this._semantics !== op.map?.semantics) {
       throw new this._client.ErrorInfo(
         `Cannot apply MAP_CREATE op on LiveMap objectId=${this.getObjectId()}; map's semantics=${this._semantics}, but op expected ${op.map?.semantics}`,
         50000,
         500,
       );
     }

-    return this._mergeInitialDataFromCreateOperation(op);
+    const update = this._mergeInitialDataFromCreateOperation(op);
+    this._client.Logger.logAction(
+      this._client.logger,
+      this._client.Logger.LOG_MICRO,
+      'LiveMap._applyMapCreate()',
+      `successfully merged MAP_CREATE op; objectId=${this._objectId}`,
+    );
+    return update;
   }
test/realtime/live_objects.test.js (1)

Line range hint 1-2112: Consider enhancing test documentation and coverage

While the test suite is comprehensive and well-structured, consider these improvements:

  1. Add detailed comments explaining the timeserial logic in complex scenarios
  2. Consider adding performance tests with large datasets to validate scalability
  3. Add tests for concurrent operations to ensure thread safety
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e151919 and 567ad15.

📒 Files selected for processing (11)
  • src/common/lib/client/realtimechannel.ts (1 hunks)
  • src/plugins/liveobjects/livecounter.ts (5 hunks)
  • src/plugins/liveobjects/livemap.ts (11 hunks)
  • src/plugins/liveobjects/liveobject.ts (4 hunks)
  • src/plugins/liveobjects/liveobjects.ts (6 hunks)
  • src/plugins/liveobjects/liveobjectspool.ts (3 hunks)
  • src/plugins/liveobjects/statemessage.ts (4 hunks)
  • src/plugins/liveobjects/syncliveobjectsdatapool.ts (2 hunks)
  • src/plugins/liveobjects/timeserial.ts (3 hunks)
  • test/common/modules/live_objects_helper.js (2 hunks)
  • test/realtime/live_objects.test.js (21 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/common/lib/client/realtimechannel.ts
  • src/plugins/liveobjects/livecounter.ts
  • src/plugins/liveobjects/syncliveobjectsdatapool.ts
  • src/plugins/liveobjects/timeserial.ts
  • test/common/modules/live_objects_helper.js
🧰 Additional context used
🪛 Biome (1.9.4)
src/plugins/liveobjects/statemessage.ts

[error] 154-154: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 158-158: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 162-162: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 166-166: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 170-170: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 195-195: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 224-224: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 230-230: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 259-259: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 266-266: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🔇 Additional comments (23)
src/plugins/liveobjects/liveobjectspool.ts (4)

6-6: LGTM! Import changes align with refactoring goals

The simplified imports reflect the removal of buffered message handling and the shift towards direct state message processing.

Also applies to: 8-8


57-57: LGTM! Simplified zero value object creation

The removal of regionalTimeserial from zero value creation aligns with the PR objectives and simplifies object initialization. Objects can now handle their own create operations independently.

Also applies to: 62-62


92-92: Fix typo in comment

"ba able" should be "be able"


90-96: Verify object initialization behavior

The new approach of having objects handle their own create operations looks good, but we should verify that all object types properly handle their initialization from create operations.

Let's verify the implementation across different object types:

src/plugins/liveobjects/liveobject.ts (6)

4-4: LGTM: Import changes align with new functionality.

The addition of StateObject to the imports is consistent with the new state object handling capabilities.


36-42: Well-documented property changes that align with optimization goals.

The new properties and documentation effectively support:

  1. Tracking site-specific timeserials (_siteTimeserials)
  2. Optimizing initial value merging (_createOperationIsMerged)
  3. Clear documentation of the aggregated value concept

Line range hint 90-102: LGTM: Optimized update notification.

Good performance optimization to prevent emitting unnecessary update events for noop changes.


133-166: Well-structured abstract interface for state management.

The abstract methods provide a clear contract for implementing classes:

  1. applyOperation: Simplified signature aligns with new state message handling
  2. overrideWithStateObject: Well-documented method for SYNC sequence handling
  3. _mergeInitialDataFromCreateOperation: Supports the optimization goal of merging initial data only once

The documentation clearly explains the rationale behind each method.


44-54: Verify handling of operations before site timeserials initialization.

While initializing _siteTimeserials as empty allows any operation to be applied (as noted in the comment), this could potentially lead to a race condition if operations arrive before proper site timeserials initialization from a SYNC sequence.


103-112: Verify edge cases in timeserial comparison logic.

The _canApplyOperation method is critical for maintaining state consistency. While the logic appears correct, we should verify handling of edge cases:

  1. Concurrent operations from the same site
  2. Operations with equal timeserials
✅ Verification successful

Edge cases in timeserial comparison are properly handled

The implementation of _canApplyOperation is correct and handles edge cases appropriately through the underlying Timeserial class implementation:

  1. Concurrent operations from same site: The _timeserialCompare method first compares timestamps, and if equal, uses site codes for deterministic ordering
  2. Operations with equal timeserials: The after() method strictly requires _timeserialCompare > 0, ensuring that equal timeserials are not applied twice

The implementation ensures consistent state by:

  • Only applying operations with strictly greater timeserials (opOriginTimeserial.after(siteTimeserial))
  • Handling the case of first operation from a site (!siteTimeserial)
  • Using a robust comparison implementation that considers both timestamp and site code
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for test cases covering timeserial edge cases
rg -A 10 "test.*timeserial.*edge" --type ts

# Look for other timeserial comparison implementations
ast-grep --pattern 'after($_, $_) {
  $$$
}'

Length of output: 92


Script:

#!/bin/bash
# Search for timeserial tests and implementation
rg -l "class Timeserial" --type ts
rg -A 10 "describe.*Timeserial" --type ts
rg -A 10 "test.*Timeserial" --type ts

# Look for actual usage of _canApplyOperation
rg "_canApplyOperation" --type ts -A 5

# Find Timeserial class implementation
ast-grep --pattern 'class Timeserial {
  $$$
}'

Length of output: 2023


Script:

#!/bin/bash
# Search for Timeserial implementation and comparison logic
rg -p "class Timeserial" --type ts -A 30
rg "after.*\(" --type ts -A 5

# Look for test files related to Timeserial
fd "timeserial.*test" --type f --exec cat {}

Length of output: 6554


Script:

#!/bin/bash
# Search for Timeserial comparison implementation
rg "_timeserialCompare" --type ts -A 10

# Look for any test files containing "Timeserial" in their content
rg -p "describe.*Timeserial|test.*Timeserial" --type ts

Length of output: 2849

src/plugins/liveobjects/liveobjects.ts (3)

27-27: LGTM: Property type simplification aligns with objectives.

The change from BufferedStateMessage[] to StateMessage[] appropriately simplifies the state handling model, supporting the removal of regional timeserials.


155-157: LGTM: Clear sequencing of sync and buffered operations.

The updated implementation and comments clearly establish the correct order: SYNC data is applied first, followed by buffered state operations. This ensures proper state consistency.


89-99: Verify message ordering preservation.

The simplified buffering logic looks good, but we should verify that message ordering is preserved when buffering and applying state messages, especially during concurrent SYNC operations.

✅ Verification successful

Let me gather more information about the message handling implementation to verify the ordering preservation.


Let me check one more aspect about the message ordering implementation in the LiveObjectsPool.


Based on the gathered evidence, I can now provide the final response:

Message ordering is preserved through the sync mechanism

The implementation correctly handles message ordering through:

  • Buffering of state messages during sync using _bufferedStateOperations
  • Sequential application of messages after sync completion in _endSync()
  • Preservation of timeserials in state messages for conflict resolution
  • Proper cleanup of buffer after message application

The removal of msgRegionalTimeserial parameter doesn't impact message ordering as the core ordering mechanism relies on the sync sequence and buffering system, with timeserials still preserved in the state messages themselves.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for message ordering guarantees in tests and related components

# Look for test cases verifying message order
rg -A 5 "test.*message.*order" --glob "**/*.test.{ts,js}"

# Check for ordering-related code in state message handling
rg -A 5 "(order|sequence|serial)" --glob "**/statemessage.{ts,js}"

# Look for any timing or race condition tests
rg -A 5 "test.*(concurrent|race|timing)" --glob "**/*.test.{ts,js}"

Length of output: 3055


Script:

#!/bin/bash
# Check LiveObjectsPool implementation for message ordering logic
ast-grep --pattern 'applyStateMessages($$$) {
  $$$
}'

# Look for buffering and sync-related logic
rg -A 10 "_bufferedStateOperations" --type ts

# Check for any message ordering tests specific to LiveObjects
rg -A 10 "test.*LiveObjects.*sync" --glob "**/*.test.{ts,js}"

Length of output: 4133


Script:

#!/bin/bash
# Check LiveObjectsPool implementation for state message handling
rg -A 15 "class LiveObjectsPool" --type ts

# Look for timeserial comparison logic
rg -A 10 "timeserial" --type ts --glob "**/liveobjectspool.ts"

# Check for any ordering-related logic in message application
rg -A 10 "applyStateMessages" --type ts --glob "**/liveobjectspool.ts"

Length of output: 3702

src/plugins/liveobjects/statemessage.ts (4)

107-114: LGTM! Interface changes align with PR objectives.

The addition of siteTimeserials and createOp fields is well-documented and supports the goals of handling site timeserials and isolated create operations.


154-170: Replace this with class name in static methods.

This issue was previously identified in past reviews.

🧰 Tools
🪛 Biome (1.9.4)

[error] 154-154: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 158-158: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 162-162: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 166-166: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 170-170: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


189-300: Well-structured helper methods improve code organization.

The introduction of private static helper methods for encoding and decoding improves code maintainability by:

  • Separating concerns into focused methods
  • Reducing code duplication
  • Making the code more testable
🧰 Tools
🪛 Biome (1.9.4)

[error] 195-195: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 224-224: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 230-230: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 259-259: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 266-266: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


320-331: LGTM! Improved JSON encoding with proper buffer handling.

The changes to toJSON method:

  • Use dedicated helper methods for consistent encoding
  • Properly handle platform-specific buffer types
  • Maintain compatibility with both JSON.stringify and msgpack
src/plugins/liveobjects/livemap.ts (1)

142-144: ⚠️ Potential issue

Consider moving site timeserials update after successful operation application

The site timeserials are updated before the operation is actually applied. If the operation application fails (e.g., throws an error), the site timeserial would still be updated, potentially causing subsequent operations to be incorrectly skipped.

Apply this diff to move the update after successful operation application:

-    // should update stored site timeserial immediately. doesn't matter if we successfully apply the op,
-    // as it's important to mark that the op was processed by the object
-    this._siteTimeserials[opOriginTimeserial.siteCode] = opOriginTimeserial;

    let update: LiveMapUpdate | LiveObjectUpdateNoop;
    switch (op.action) {
      case StateOperationAction.MAP_CREATE:
        update = this._applyMapCreate(op);
        break;
      // ... rest of the switch
    }

    this.notifyUpdated(update);
+    // Update site timeserial after successful operation application
+    this._siteTimeserials[opOriginTimeserial.siteCode] = opOriginTimeserial;
test/realtime/live_objects.test.js (5)

33-46: Well-structured helper function implementation!

The forScenarios helper function provides a clean way to handle test scenario execution with support for "only" and "skip" functionality. This improves test organization and maintainability.


Line range hint 504-1002: Comprehensive test coverage for state operations!

The test scenarios thoroughly cover:

  • MAP_CREATE operations with primitives and object IDs
  • MAP_SET operations with various data types
  • MAP_REMOVE operations
  • Site timeserials vector validation
  • Edge cases and error conditions

The tests are well-structured with clear descriptions and robust assertions.


Line range hint 1003-1577: Excellent test coverage for sync sequences!

The test scenarios effectively validate:

  • Operation buffering during STATE_SYNC
  • Proper application of buffered operations
  • Site timeserials vector handling during sync
  • Edge cases for sync sequence interruption

The tests provide robust coverage of complex synchronization scenarios.


Line range hint 1578-2088: Well-structured subscription test coverage!

The test scenarios thoroughly validate:

  • LiveCounter and LiveMap subscription callbacks
  • Multiple subscription handling
  • Unsubscribe functionality (both individual and all)
  • Update notification accuracy

The tests provide comprehensive coverage of the subscription lifecycle.


Line range hint 2089-2112: Clean channel attachment test implementation!

The test effectively validates:

  • Channel attachment with LiveObjects state modes
  • Proper channel options configuration
  • Mode validation

Simple and focused test case that covers the essential functionality.

@github-actions github-actions bot temporarily deployed to staging/pull/1924/features November 22, 2024 17:29 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1924/typedoc November 22, 2024 17:29 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1924/bundle-report November 22, 2024 17:30 Inactive
Refactors LiveMap/LiveCounter object creation and moves most of the
creation related busy work inside those classes.

Resolves DTP-1076
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (10)
src/plugins/liveobjects/liveobjectspool.ts (1)

Line range hint 69-96: Consider documenting the new state management flow.

The simplified state management approach is a significant architectural improvement. Consider adding high-level documentation (e.g., in README.md or API docs) explaining:

  1. The lifecycle of state operations
  2. How zero-value objects work
  3. The relationship between create operations and object initialization

This will help future maintainers understand the design decisions.

src/plugins/liveobjects/liveobject.ts (3)

103-112: Consider adding error handling for invalid timeserials

The _canApplyOperation method's logic is sound, but it might benefit from additional validation.

 protected _canApplyOperation(opOriginTimeserial: Timeserial): boolean {
+  if (!opOriginTimeserial?.siteCode) {
+    this._client.logger.warn('Invalid timeserial received');
+    return false;
+  }
   const siteTimeserial = this._siteTimeserials[opOriginTimeserial.siteCode];
   return !siteTimeserial || opOriginTimeserial.after(siteTimeserial);
 }

114-125: Consider memoizing timeserial conversion results

The _timeserialMapFromStringMap method performs well but could benefit from memoization for frequently accessed timeserials.

+private _timeserialCache: Map<string, Timeserial> = new Map();
+
 protected _timeserialMapFromStringMap(stringTimeserialsMap: Record<string, string>): Record<string, Timeserial> {
   const objTimeserialsMap = Object.entries(stringTimeserialsMap).reduce(
     (acc, v) => {
       const [key, timeserialString] = v;
-      acc[key] = DefaultTimeserial.calculateTimeserial(this._client, timeserialString);
+      if (!this._timeserialCache.has(timeserialString)) {
+        this._timeserialCache.set(
+          timeserialString,
+          DefaultTimeserial.calculateTimeserial(this._client, timeserialString)
+        );
+      }
+      acc[key] = this._timeserialCache.get(timeserialString)!;
       return acc;
     },
     {} as Record<string, Timeserial>,
   );

   return objTimeserialsMap;
 }

127-132: Consider implementing the TODO or creating a tracking issue

The TODO comment about object ID generation implementation should be addressed.

Would you like me to help create a GitHub issue to track this TODO implementation?

src/plugins/liveobjects/liveobjects.ts (1)

197-201: Consider adding detailed error handling for state object conversion

While the implementation is solid, consider adding more detailed error handling for cases where overrideWithStateObject might fail. This could help with debugging synchronization issues in production.

-        const update = existingObject.overrideWithStateObject(entry.stateObject);
+        let update: LiveObjectUpdate;
+        try {
+          update = existingObject.overrideWithStateObject(entry.stateObject);
+        } catch (error) {
+          this._client.Logger.logAction(
+            this._client.logger,
+            this._client.Logger.LOG_ERROR,
+            'LiveObjects._applySync()',
+            `Failed to override state object for ${objectId}: ${error}`,
+          );
+          throw error;
+        }
src/plugins/liveobjects/statemessage.ts (1)

107-114: LGTM! Well-documented interface changes.

The replacement of regionalTimeserial with siteTimeserials and addition of createOp align well with the PR objectives. The changes improve the handling of operations and initial values.

Consider adding a migration guide or deprecation notice if this is a breaking change, as existing consumers might need to update their code to handle the new siteTimeserials structure.

test/realtime/live_objects.test.js (4)

33-46: Consider enhancing the forScenarios helper with error handling and logging.

The helper function provides good test filtering functionality but could be improved by:

  1. Adding error handling for invalid scenario objects
  2. Including debug logging for skipped/selected scenarios
  3. Adding TypeScript types for better maintainability
  function forScenarios(scenarios, testFn) {
+   if (!Array.isArray(scenarios)) {
+     throw new Error('scenarios must be an array');
+   }
+
    const onlyScenarios = scenarios.filter((x) => x.only === true);
    const scenariosToRun = onlyScenarios.length > 0 ? onlyScenarios : scenarios;

    for (const scenario of scenariosToRun) {
      if (scenario.skip === true) {
+       console.debug(`Skipping scenario: ${scenario.description}`);
        continue;
      }
+     console.debug(`Running scenario: ${scenario.description}`);
      testFn(scenario);
    }
  }

638-714: Consider adding edge cases to site timeserials vector tests.

While the test coverage for site timeserials is good, consider adding these scenarios:

  1. Concurrent operations from multiple sites
  2. Network partition scenarios
  3. Error cases for invalid timeserials

Line range hint 1377-1486: Consider refactoring complex sync test scenarios for better maintainability.

The test scenarios for buffered operations with site timeserials are complex and could benefit from:

  1. Extracting common setup code into helper functions
  2. Using a data-driven approach for test cases
  3. Adding more descriptive comments for the timeserial logic
  description:
    'buffered state operation messages are applied based on the site timeserials vector of the object',
  action: async (ctx) => {
    const { root, liveObjectsHelper, channel } = ctx;

+   // Helper function to create test state with timeserials
+   const createTestState = (mapId, counterId) => ({
+     mapObject: liveObjectsHelper.mapObject({
+       objectId: mapId,
+       siteTimeserials: {
+         bbb: 'bbb@2-0',
+         ccc: 'ccc@5-0',
+       },
+       // ... rest of the state
+     }),
+     // ... other objects
+   });

    // start new sync sequence with a cursor
    const mapId = liveObjectsHelper.fakeMapObjectId();
    const counterId = liveObjectsHelper.fakeCounterObjectId();
    await liveObjectsHelper.processStateObjectMessageOnChannel({
      channel,
      syncSerial: 'serial:cursor',
-     state: [
-       liveObjectsHelper.mapObject({
-         objectId: mapId,
-         siteTimeserials: {
-           bbb: 'bbb@2-0',
-           ccc: 'ccc@5-0',
-         },
-         // ... rest of the state
-       }),
-       // ... other objects
-     ],
+     state: createTestState(mapId, counterId),
    });
    // ... rest of the test
  },

Line range hint 1698-1731: Add error handling test cases for subscription callbacks.

The subscription tests should include scenarios for:

  1. Error handling in callbacks
  2. Memory leak prevention
  3. Race conditions during unsubscribe

Consider adding these test cases:

{
  description: 'handles errors in subscription callbacks gracefully',
  action: async (ctx) => {
    const { root, map } = ctx;
    const errorSpy = sinon.spy(console, 'error');
    
    map.subscribe(() => {
      throw new Error('Callback error');
    });
    
    // Verify error is caught and logged
    expect(errorSpy.called).to.be.true;
    errorSpy.restore();
  }
},
{
  description: 'prevents memory leaks from subscription callbacks',
  action: async (ctx) => {
    // Test cleanup of callback references
  }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 059e10b and 245b6a2.

📒 Files selected for processing (9)
  • src/plugins/liveobjects/livecounter.ts (5 hunks)
  • src/plugins/liveobjects/livemap.ts (11 hunks)
  • src/plugins/liveobjects/liveobject.ts (4 hunks)
  • src/plugins/liveobjects/liveobjects.ts (6 hunks)
  • src/plugins/liveobjects/liveobjectspool.ts (3 hunks)
  • src/plugins/liveobjects/statemessage.ts (4 hunks)
  • src/plugins/liveobjects/syncliveobjectsdatapool.ts (2 hunks)
  • test/common/modules/live_objects_helper.js (2 hunks)
  • test/realtime/live_objects.test.js (21 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/plugins/liveobjects/livecounter.ts
  • src/plugins/liveobjects/syncliveobjectsdatapool.ts
  • test/common/modules/live_objects_helper.js
🔇 Additional comments (19)
src/plugins/liveobjects/liveobjectspool.ts (4)

6-6: LGTM! Import changes align with the simplified state management approach.

The removal of BufferedStateMessage and Timeserial imports reflects the architectural changes described in the PR objectives.

Also applies to: 8-8


90-94: Well-documented architectural improvement.

The new approach of creating zero-value objects upfront and letting objects handle their own create operations is cleaner and more maintainable. The comments effectively explain the rationale behind this design decision.


57-57: Verify zeroValue implementations in LiveMap and LiveCounter.

The changes look good, but let's verify the static factory methods exist and are properly implemented.

Also applies to: 62-62

✅ Verification successful

Static zeroValue methods are properly implemented in both LiveMap and LiveCounter

Both classes have the required static factory methods with correct signatures and implementations:

  • LiveMap.zeroValue(liveobjects: LiveObjects, objectId: string) creates a new LiveMap with LWW semantics
  • LiveCounter.zeroValue(liveobjects: LiveObjects, objectId: string) creates a new LiveCounter instance
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify zeroValue static methods exist in LiveMap and LiveCounter

# Check LiveMap implementation
ast-grep --pattern 'class LiveMap {
  $$$
  static zeroValue($_) {
    $$$
  }
  $$$
}'

# Check LiveCounter implementation
ast-grep --pattern 'class LiveCounter {
  $$$
  static zeroValue($_) {
    $$$
  }
  $$$
}'

Length of output: 183


Script:

#!/bin/bash
# Let's try a broader search for LiveMap and LiveCounter classes first
echo "=== Searching for LiveMap class ==="
rg "class LiveMap" -A 5

echo "=== Searching for LiveCounter class ==="
rg "class LiveCounter" -A 5

echo "=== Searching for zeroValue method mentions ==="
rg "zeroValue" -A 2

Length of output: 3570


96-96: Verify error handling for invalid object creation.

We should ensure proper error handling exists when operations arrive for objects that shouldn't be created.

✅ Verification successful

Let me gather more information about error handling in the LiveObjectsPool class since that's where the get method is called.


Let me fix the regex and try again to gather the necessary information.


From the results, I can see that the LiveObjectsPool class handles object creation and operations. Let me check one more aspect about error handling in the ObjectId class since it's used for validation.


Error handling for invalid object creation is properly implemented

The codebase has comprehensive error handling for invalid object creation:

  • ObjectId validation ensures proper format and type checking with appropriate error messages
  • LiveMap and LiveCounter implementations validate operations against object IDs
  • LiveObjectsPool creates objects only after validating through ObjectId.fromString
  • Non-existent objects are handled via the undefined return type in the get method
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for error handling in applyOperation implementations

# Look for error handling in LiveMap and LiveCounter
rg -A 5 "applyOperation.*\{" src/plugins/liveobjects/live{map,counter}.ts

# Check for validation in object creation
rg -A 5 "zeroValue.*\{" src/plugins/liveobjects/live{map,counter}.ts

Length of output: 2148


Script:

#!/bin/bash
# Check error handling in LiveObjectsPool
rg -A 10 "get\(.*\).*{" src/plugins/liveobjects/liveobjectspool.ts

# Check how objects are created/initialized
rg -A 10 "createObject" src/plugins/liveobjects/liveobjectspool.ts

# Look for any error handling around object creation
rg -A 5 "throw.*Error" src/plugins/liveobjects/liveobjectspool.ts

Length of output: 291


Script:

#!/bin/bash
# Check error handling in LiveObjectsPool's get method
rg -A 10 "get\s*\(" src/plugins/liveobjects/liveobjectspool.ts

# Check object creation and initialization
ast-grep --pattern 'class LiveObjectsPool {
  $$$
  get($_) {
    $$$
  }
  $$$
}'

# Look for error handling in LiveObjectsPool
rg -A 5 "throw" src/plugins/liveobjects/liveobjectspool.ts

Length of output: 1476


Script:

#!/bin/bash
# Check ObjectId validation and error handling
rg -A 5 "fromString" src/plugins/liveobjects/objectid.ts

# Look for type validation
rg -A 5 "type.*=.*" src/plugins/liveobjects/objectid.ts

# Check for error handling in object creation
rg -A 5 "throw" src/plugins/liveobjects/objectid.ts

Length of output: 1170

src/plugins/liveobjects/liveobject.ts (4)

4-4: LGTM! Well-documented property additions

The new imports and property declarations are clear and well-documented. The _dataRef documentation effectively explains its role in combining initial and state operations.

Also applies to: 36-42


Line range hint 90-101: LGTM! Improved update notification logic

The noop check optimization prevents unnecessary event emissions, improving performance.


137-166: LGTM! Well-documented abstract method changes

The abstract method changes are well-documented and align perfectly with the PR objectives. The documentation clearly explains the rationale behind merging initial data only once, which should improve performance as intended.


44-54: Verify impact of constructor parameter removal

The constructor changes look good, with proper initialization of new properties. However, since initialData parameter was removed, we should verify this doesn't break existing instantiations.

✅ Verification successful

Let me verify if the LiveObject class is actually meant to be directly instantiated or if it's an abstract base class. This will help determine if the constructor parameter removal has any impact.


Let me try a different approach to check the LiveObject class definition and its usage:


Based on the results, I can now generate the final response:

Constructor changes are safe and well-implemented

The removal of the initialData parameter from the constructor is safe because:

  • LiveObject is an abstract class (marked with abstract keyword)
  • No direct instantiations of LiveObject were found in the codebase
  • The constructor is protected, preventing direct instantiation from outside the class hierarchy
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for direct LiveObject instantiations that might be affected
rg -l "new LiveObject.*\(" | while read -r file; do
  echo "=== $file ==="
  rg "new LiveObject.*\(" "$file" -A 2
done

Length of output: 1430


Script:

#!/bin/bash
# Check if LiveObject is abstract and how it's used
ast-grep --pattern 'class LiveObject'

Length of output: 39


Script:

#!/bin/bash
# Search for LiveObject class definition and check if it's abstract
rg "class LiveObject" -B 2 -A 2

# Also check for any direct instantiations using 'new LiveObject('
rg "new LiveObject\(" -B 2 -A 2

Length of output: 1334

src/plugins/liveobjects/liveobjects.ts (4)

89-99: LGTM! Clean implementation of state message handling

The simplified implementation correctly handles the concurrent nature of state messages during SYNC sequence. The removal of regional timeserial parameter aligns with the PR objectives while maintaining the necessary buffering logic.


155-157: LGTM! Clear ordering of operations

The implementation maintains the correct synchronization order by applying buffered operations after SYNC data, with clear documentation of the process.


Line range hint 198-214: Verify the implementation of new state object methods

The changes introduce new methods:

  • overrideWithStateObject for existing objects
  • Static fromStateObject factory methods for LiveCounter and LiveMap

These changes align with the PR objectives by consistently using StateObject.

✅ Verification successful

Let me try a different approach to verify these methods.


Implementation of state object methods verified successfully

The code shows proper implementation of both methods across LiveCounter and LiveMap:

  • Both classes implement static fromStateObject factory method that:

    • Creates a new instance
    • Calls overrideWithStateObject with the state object
    • Returns the initialized instance
  • Both implement overrideWithStateObject with proper validation:

    • Verifies objectId matches
    • Handles state object data appropriately
    • Returns corresponding update objects (LiveCounterUpdate/LiveMapUpdate)

The implementation aligns with the PR changes and maintains consistency in state object handling.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify the implementation of new methods
echo "Checking LiveCounter implementation:"
ast-grep --pattern 'class LiveCounter {
  $$$
  static fromStateObject($$$) {
    $$$
  }
  $$$
  overrideWithStateObject($$$) {
    $$$
  }
}'

echo "Checking LiveMap implementation:"
ast-grep --pattern 'class LiveMap {
  $$$
  static fromStateObject($$$) {
    $$$
  }
  $$$
  overrideWithStateObject($$$) {
    $$$
  }
}'

Length of output: 445


Script:

#!/bin/bash
# Search for LiveCounter and LiveMap implementations
echo "Searching for LiveCounter implementation:"
rg "class LiveCounter" -A 20

echo "\nSearching for LiveMap implementation:"
rg "class LiveMap" -A 20

echo "\nSearching for overrideWithStateObject implementations:"
rg "overrideWithStateObject" -A 10

Length of output: 8558


27-27: Verify the impact of BufferedStateMessage removal

The change from BufferedStateMessage[] to StateMessage[] simplifies the buffering mechanism by removing the regional timeserial wrapper. This is aligned with the PR objectives.

✅ Verification successful

Based on the search results, I can now generate the final response:

Change from BufferedStateMessage[] to StateMessage[] is safe

The change from BufferedStateMessage[] to StateMessage[] is a safe simplification as there are no references to BufferedStateMessage type in the codebase. The StateMessage class is the core type used throughout the LiveObjects implementation for state synchronization, and the removal of the buffer wrapper aligns with the PR objectives of removing regional timeserial.

Key observations:

  • StateMessage is properly defined and used consistently across the codebase
  • No other components depend on BufferedStateMessage type
  • The change simplifies the data structure without breaking functionality
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to BufferedStateMessage to ensure complete removal
rg "BufferedStateMessage" --type ts

Length of output: 35


Script:

#!/bin/bash
# Let's check the git diff to see the changes related to BufferedStateMessage
git diff origin/main -- '*.ts' | grep -A 5 -B 5 "BufferedStateMessage"

# Also search for any type definitions or interfaces that might be related
ast-grep --pattern 'interface $_ {
  $$$
}'

# And check for any type definitions
ast-grep --pattern 'type $_ = $$$'

Length of output: 100785


Script:

#!/bin/bash
# Let's check for any state message related types and interfaces
ast-grep --pattern 'type StateMessage = $$$'

# Also search for any state message related code
rg "StateMessage" --type ts -A 3 -B 3

Length of output: 23192

src/plugins/liveobjects/statemessage.ts (3)

189-211: LGTM! Well-structured decode helper methods.

The helper methods effectively encapsulate the decoding logic, improving maintainability.


337-348: LGTM! Clean refactoring of toJSON method.

The use of static helper methods improves code organization while maintaining the correct handling of base64 encoding.


154-170: Clarify error handling strategy in TODO comment.

The TODO comment about error handling is vague. Consider documenting:

  1. Why throwing the first error is the chosen strategy
  2. What types of errors can occur
  3. How this affects the partial decoding of the message

Let's check for similar error handling patterns in the codebase:

src/plugins/liveobjects/livemap.ts (4)

132-144: LGTM! Well-structured timeserial handling

The implementation correctly handles operation timeserials and site timeserials updates. The logging is helpful for debugging skipped operations.


72-75: ⚠️ Potential issue

Remove unsafe non-null assertion

The non-null assertion (!) on stateObject.map?.semantics is unsafe and could lead to runtime errors if map is undefined.

Apply this diff to safely handle undefined map:

-  static fromStateObject(liveobjects: LiveObjects, stateObject: StateObject): LiveMap {
-    const obj = new LiveMap(liveobjects, stateObject.map?.semantics!, stateObject.objectId);
-    obj.overrideWithStateObject(stateObject);
-    return obj;
+  static fromStateObject(liveobjects: LiveObjects, stateObject: StateObject): LiveMap {
+    if (!stateObject.map?.semantics) {
+      throw new liveobjects.ErrorInfo(
+        `Invalid state object: map or semantics is undefined for objectId=${stateObject.objectId}`,
+        50000,
+        500,
+      );
+    }
+    const obj = new LiveMap(liveobjects, stateObject.map.semantics, stateObject.objectId);
+    obj.overrideWithStateObject(stateObject);
+    return obj;
+  }

Likely invalid or redundant comment.


456-484: ⚠️ Potential issue

Add validation for entry.data

The method accesses entry.data properties without verifying if entry.data is defined. This could cause runtime errors.

Apply this diff to add the necessary validation:

  private _liveMapDataFromMapEntries(entries: Record<string, StateMapEntry>): LiveMapData {
    const liveMapData: LiveMapData = {
      data: new Map<string, MapEntry>(),
    };

    Object.entries(entries ?? {}).forEach(([key, entry]) => {
+     if (!entry.data) {
+       throw new this._client.ErrorInfo(
+         `Invalid entry data: data is undefined for key=${key} in objectId=${this.getObjectId()}`,
+         50000,
+         500,
+       );
+     }
      let liveData: StateData;
      if (typeof entry.data.objectId !== 'undefined') {
        liveData = { objectId: entry.data.objectId } as ObjectIdStateData;
      } else {
        liveData = { encoding: entry.data.encoding, value: entry.data.value } as ValueStateData;
      }

      const liveDataEntry: MapEntry = {
        ...entry,
        timeserial: entry.timeserial
          ? DefaultTimeserial.calculateTimeserial(this._client, entry.timeserial)
          : DefaultTimeserial.zeroValueTimeserial(this._client),
        tombstone: entry.tombstone === true,
        data: liveData,
      };

      liveMapData.data.set(key, liveDataEntry);
    });

    return liveMapData;
  }

Likely invalid or redundant comment.


195-227: ⚠️ Potential issue

Remove unsafe optional chaining and non-null assertions

The validation logic uses unsafe optional chaining with non-null assertions that could lead to runtime errors.

Apply this diff to safely handle undefined values:

-    if (stateObject.map?.semantics !== this._semantics) {
+    if (!stateObject.map) {
+      throw new this._client.ErrorInfo(
+        `Invalid state object: map is undefined for objectId=${stateObject.objectId}`,
+        50000,
+        500,
+      );
+    }
+    if (stateObject.map.semantics !== this._semantics) {
      throw new this._client.ErrorInfo(
-        `Invalid state object: state object map semantics=${stateObject.map?.semantics}; LiveMap semantics=${this._semantics}`,
+        `Invalid state object: state object map semantics=${stateObject.map.semantics}; LiveMap semantics=${this._semantics}`,
        50000,
        500,
      );
    }

    if (!this._client.Utils.isNil(stateObject.createOp)) {
      // ... existing createOp validation ...

-      if (stateObject.createOp.map?.semantics !== this._semantics) {
+      if (!stateObject.createOp.map) {
+        throw new this._client.ErrorInfo(
+          `Invalid state object: createOp map is undefined for objectId=${this.getObjectId()}`,
+          50000,
+          500,
+        );
+      }
+      if (stateObject.createOp.map.semantics !== this._semantics) {
        throw new this._client.ErrorInfo(
-          `Invalid state object: state object createOp map semantics=${stateObject.createOp.map?.semantics}; LiveMap semantics=${this._semantics}`,
+          `Invalid state object: state object createOp map semantics=${stateObject.createOp.map.semantics}; LiveMap semantics=${this._semantics}`,
          50000,
          500,
        );
      }
    }

Likely invalid or redundant comment.

src/plugins/liveobjects/statemessage.ts Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants