@fluidframework/id-compressor 2.41.0-338401 → 2.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @fluidframework/id-compressor
2
2
 
3
+ ## 2.41.0
4
+
5
+ Dependency updates only.
6
+
3
7
  ## 2.40.0
4
8
 
5
9
  Dependency updates only.
package/README.md CHANGED
@@ -40,156 +40,127 @@ API documentation for **@fluidframework/id-compressor** is available at <https:/
40
40
 
41
41
  <!-- AUTO-GENERATED-CONTENT:END -->
42
42
 
43
- ## Overview
43
+ ## Introduction
44
44
 
45
- The distributed ID allocation scheme allows clients to use, store, and reference small numbers that map to UUIDs rather than the UUIDs themselves. The primary benefits are improved memory efficiency and reduced storage size, as well as improved runtime performance when used as keys in collections.
45
+ The **Fluid Framework ID Compressor** (package `@fluidframework/id-compressor`) provides a distributed unique ID generation and compression scheme. It allows multiple clients (“sessions”) to generate **stable IDs** (v4 UUIDs) that are globally unique, while representing them as small integers locally. This yields significant benefits in memory and storage efficiency and faster lookups (e.g., using compressed IDs as keys) compared to using full UUID strings. The compressor works in tandem with a centralized ordering service (the Fluid runtime’s total order broadcast) to assign each generated ID a **final** numeric value that is unique within a shared **document** (collaboration session).
46
46
 
47
- The scheme requires a total order for ID allocation, so the allocator is designed to work within a service that provides centralized total order broadcast.
47
+ Each client session uses the compressor to synchronously generate IDs without coordination, possibly using a temporary **session-local form** (negative integer) that is only unique within that client. Later, when the operation defining the ID is sequenced by the service, the ID is assigned a permanent **final form** (a non-negative integer) that is globally unique within the document. An ID’s numeric **sign** (negative or non-negative) at creation depends on whether a final value was immediately assigned (which in turn depends on the state of the allocator at that moment) – once created, an ID’s form remains stable within that session’s context and does not change implicitly. The compressor’s API provides explicit normalization functions to convert IDs between the **session space** (local view) and **op space** (final view), as needed.
48
48
 
49
- ### Sessions
49
+ ## Key Concepts
50
50
 
51
- A session can be thought of as a unique identifier for a single allocator. Even if an allocator is taken offline, serialized to disk, and rehydrated back, it's considered the same session if it has the same session ID.
51
+ ### Sessions and Documents
52
52
 
53
- ### Document
53
+ A **session** refers to a unique identifier for an individual ID compressor (typically one client or one component of the app). Each session has its own local ID sequence. Even if a compressor is saved and restored (e.g., for offline use), as long as it retains the same session identifier, it is considered the same logical session. All compressors operating on the same collaborative document share the same ID space after finalization, but before finalization they may use overlapping local IDs distinguished by session.
54
54
 
55
- The set of IDs generated by all Sessions contributing to the same distributed collection (e.g. an individual collaborative document). Uniqueness of allocated IDs is limited to the document context (unless in their UUID form), and thus IDs in their compressed form are not portable between documents.
55
+ A **document** in this context is the collection of all IDs generated by all sessions collaborating on a particular Fluid container. Compressed IDs are only unique within the scope of a single document. (In their full UUID form, they remain globally unique, but the small integer representations are **not** intended to be compared or merged across different documents.)
56
56
 
57
- ### Generation API
57
+ ### Stable IDs and Compressed IDs
58
58
 
59
- New IDs are generated via a synchronous API on the allocator. The API returns a small number representation of the UUID with the following guarantees:
59
+ A **stable ID** is the full 128-bit UUID string that serves as the true globally unique identifier. The ID Compressor generates stable IDs in a deterministic, low-entropy way: each session chooses a random 128-bit starting UUID when it begins, and thereafter generates subsequent UUIDs by incrementing a counter (essentially producing sequential UUIDs). This strategy greatly reduces entropy in the generated IDs (making them more compressible and easier to store) while **improving collision resistance** relative to fully random generation. In fact, by allocating each session a distinct block of the UUID space starting at a random base, the probability of any two sessions ever producing the same UUID is astronomically low – even lower than if all IDs were purely random – since only the initial UUID selection is random and subsequent IDs from the same session are guaranteed unique increments.
60
60
 
61
- - The ID can be synchronously converted into a full UUID at any time. This is required if global uniqueness (across documents) is needed.
62
- - Prior to the service being notified of the ID's creation, it is unique within the scope of the session (i.e. the new ID will not collide with other IDs from the same allocator, but it can collide with IDs from a different allocator in the same network).
63
- - After the service has been notified of the ID's creation and has determined a total ordering between all clients creating IDs, the ID will have a "final" form that is a small number representation that is unique across the [document](#document).
61
+ A **compressed ID** is the small integer representation of a stable ID used within the document. The compressor deals with two kinds of compressed IDs:
64
62
 
65
- ### ID Spaces
63
+ * **Session-space IDs:** An ID in **session space** is in its most local form. For the session that created the ID, this means if the ID has not been finalized yet it remains in a local form (denoted by a negative number in the current implementation), and if it was created with a final assignment, it will be that final number (non-negative). For any other session, a session-space ID for an ID they didn’t create would appear as a final number or not exist at all in their local space. In short, in session space each compressor sees its own IDs in local form when possible. Application code should use session-space IDs when referring to IDs internally, since these IDs are stable for the lifetime of that compressor and avoid confusion between local vs final forms.
64
+ * **Op-space IDs:** An ID in **op space** is normalized to the most **final** form known. This means that any ID which has a final (document-wide) assignment will be represented by that final (non-negative) number. Only IDs that are not yet sequenced will appear in their session-local form (and those are only meaningful if accompanied by the session that created them). Op-space IDs are used when serializing IDs for communication (ops) or persistence, as they are as close to final as possible and often require minimal or no further normalization on the receiving side. However, op-space IDs may still include un-finalized (session-scoped) IDs if a client sent them before those were ordered; therefore, any op containing op-space IDs must indicate the originating session so that receivers can interpret those IDs correctly.
66
65
 
67
- The allocation scheme separates the small-number IDs into two "spaces":
66
+ **Important:** The compressor’s implementation uses **negative integers** to represent session-local IDs and **non-negative integers** to represent final IDs. For example, when a client generates a new ID that has not yet been ordered by the server, it might be represented as –1, –2, etc. within that session. Once finalized, that ID will have a non-negative integer (e.g., 0, 1, 42, etc.) as its final form. A given compressor will always refer to that ID by the same numeric value in session space (e.g., –2) for its lifetime – it will not implicitly “flip” to the final number. To avoid bugs, one should not mix the two spaces without normalization: e.g., do not use an op space ID to index a data structure keyed by session space IDs (and indeed never use op space IDs for anything other than serialized transportation). The compressor provides normalization APIs to convert between these spaces when needed.
68
67
 
69
- 1. Session space: IDs are unique within the session scope, but are not guaranteed to be [document](#document) unique without the accompanying context of their session ID.
70
- 2. Op space: IDs are in their most final form, as unique as possible within the entire system. IDs that have been ordered by the central service ("finalized") are represented in their final form, while any other IDs that have not yet been finalized are left in their "session space" form.
68
+ ### ID Allocation and Lifecycle State Machine
71
69
 
72
- Each of these spaces is represented by a discrete type. This allows for the direct encoding of an ID's uniqueness guarantees without contextual ambiguity. An ID can be "normalized" from one space to the other with a sychronous call.
70
+ At a high level, the ID Compressor partitions the infinite universe of small integers among all sessions via **clusters** of IDs, allowing each session to carve out contiguous blocks of the positive integer space for its own use. This partitioning is dynamic and driven by the sequencing of operations. Each session can generate IDs independently using negative placeholders, which enables immediate, **synchronous creation** of new IDs without waiting for coordination. Later, when the server assigns an order, the session is awarded a block of positive IDs (a cluster) to cover those creations. Subsequent creations by that session can then directly use those reserved positive IDs (no placeholder needed) until the block is exhausted. In this way, sessions alternate between using pre-allocated final IDs (when available) and using local negative IDs (when they run out of pre-allocated space), depending on the timing of acknowledgements.
73
71
 
74
- ### Usage Guidelines
72
+ **State Machine Overview:** Each ID generated by the system goes through the following states or transitions:
75
73
 
76
- When delivering Allocated IDs to application authors for use in place of V4 UUIDs, follow these guidelines:
74
+ 1. **Generated (Session-Local)**: When a new ID is created via `generateCompressedId()` on a compressor, it belongs to that compressor’s local session. If the session currently has **no reserved final IDs available**, the new ID will be assigned a **session-local (negative) ID**. This means the ID is unique only in the context of that session and awaits finalization. If instead the session already has a cluster of final IDs reserved (from a previously finalized range) with unused capacity, the compressor will immediately assign the new ID the next available **final (non-negative) ID** in that cluster. In this case, the ID’s final value is known *a priori*. The compressor can do this safely because the cluster was obtained via a prior operation that will be sequenced before any operation that uses these final IDs, preserving consistency.
77
75
 
78
- - Expose only session space IDs to application authors. This means that application developers don't need to manage multiple ID spaces or worry about converting IDs between session space and op space. This simplifies the data management process and reduces the chances of errors or inconsistencies.
76
+ 2. **Sequenced (Finalizing)**: After one or more IDs are generated (some possibly in local form), the client’s next operation will include the **creation range** of new IDs. The runtime internally calls `takeNextCreationRange()` to obtain a batch of newly generated IDs and their details. This range, containing the session’s ID context (session ID and a contiguous generation index range, plus a hint for cluster size), is sent to the server and then relayed to all clients. It’s crucial that ranges are sent in the order they were taken from the compressor to maintain consistency. At the point of sequencing, the ID Compressor on each client **finalizes** that range of IDs via `finalizeCreationRange()`. Finalization allocates a block of **final ID numbers** (a cluster) for the session if one doesn’t already exist, or extends an existing cluster if possible, and maps each session-local ID in the range to a specific final ID. After this, those IDs have a permanent document-unique numeric identity.
79
77
 
80
- - **Note:** Session space IDs are only unique within the originating session (for most applications this will mean within each individual client's app instance, as it is rare to hold handles to more than one allocator). This constrains usage to scopes where IDs are unique; for instance, application developers should never persist these IDs and should instead be directed to convert them to stable IDs first.
78
+ 3. **Final (Document-Unique)**: Once finalized, an ID’s **final compressed ID** is known to all participants and will be used in all contexts requiring document-wide uniqueness (e.g., in ops, storage, other clients’ local state). The ID Compressor guarantees that a given session-local ID always maps to the same final ID on all clients. It also guarantees that the stable UUID associated with the ID remains the same throughout. In other words, after finalization, everyone agrees on the mapping: e.g., session X’s local ID –1 corresponds to final ID 100 (which in turn corresponds to a particular UUID). The local session that created the ID can still refer to it by –1 in session space, but it could also obtain 100 by normalizing to op space. Other sessions will refer to it as 100 in both session and op space (since for them it’s not a local ID at all, but a finalized foreign ID).
81
79
 
82
- - Use op space IDs for serialized forms, such as sending information over the wire or storing it in a file. Op space IDs are always generated in their _final_ form when possible, which imparts a performance benefit by reducing normalization work when a client receives a network operation or rehydrates an allocator.
83
- - When serializing op space IDs, annotate the entire context (e.g., file or network operation) with the session ID. This is necessary because not all IDs in op space are in their final form. Any non-finalized IDs will still be in their session space form, unique only within that specific session. Annotating the entire context with the session ID ensures that recipients of the serialized data can correctly interpret and process the non-finalized IDs.
80
+ Because each session reserves contiguous **clusters** of the final ID space when finalizing ranges, the final IDs produced by different sessions are **sharded** (partitioned) across the number line. For example, session A might get a cluster of final IDs `[0..99]` for its first 100 IDs, session B then gets `[100..199]` for its IDs, etc., depending on ordering. These assignments are monotonic; no two sessions will ever produce the same final ID, and each cluster is owned by one session. Over time, a session may have multiple clusters (if it generated IDs at different times interleaved with other sessions’ generations), forming that session’s **cluster chain**.
84
81
 
85
- ### Efficiency Properties
82
+ **Cluster Chains:** A **cluster** is a contiguous range of final IDs reserved for a session, represented by a base final ID and a capacity (number of IDs in that range). A session’s cluster chain is the sequence of all clusters assigned to that session in the order they were created (which is also ascending order of final IDs for that session). The cluster chain enables efficient mapping from a session-local ID to the final ID: within one cluster, the mapping is an offset. For instance, if session A’s cluster has base final ID 0 and capacity 5, and it’s the first cluster (hence base local ID –1), then A’s local IDs `-1, -2, -3, -4, -5` correspond to final IDs `0, 1, 2, 3, 4` respectively. If session A later gets a second cluster with base final ID 10 and capacity 5, with base local ID –6 (meaning A had generated 5 IDs prior), then A’s `-6, -7, -8, -9, -10` will map to final IDs `10, 11, 12, 13, 14` respectively. Each cluster thus stores only two integers – its starting final ID and its size – which is a very compact representation. Given a session-local ID, the compressor can determine which cluster’s range it falls into (via binary search on the cluster chain) and compute the final ID by offset, or determine that it’s not finalized yet if it falls beyond the last finalized local. This cluster structure is central to the compressor’s space efficiency and speed.
86
83
 
87
- #### UUID Generation
84
+ **Why Clusters?** The use of cluster chains serves multiple purposes:
88
85
 
89
- The allocator generates UUIDs in non-random ways to reduce entropy, optimizing storage of data. A session begins with a random UUID, and subsequent IDs are allocated sequentially. UUIDs generated with this strategy are less likely to collide than fully random UUIDs, and this fact can be leveraged to compact the on-disk and in-memory representations of the allocator.
86
+ * **Uniqueness and Sharding:** By allocating disjoint blocks of the final ID space to each session, the scheme avoids per-ID coordination – sessions don’t step on each other’s IDs because each session uses a different numeric range. Final IDs are essentially **sharded** by session ownership.
87
+ * **Compact Mapping:** Instead of storing a mapping for each ID, the compressor only needs to store cluster descriptors. If a session generates a large sequence of IDs, they will likely fall into a few contiguous clusters. Each cluster covers many IDs, so the memory overhead is low (two numbers per cluster).
88
+ * **Local Generation without Penalty:** Negative session-local IDs allow an app to mint an unbounded number of new IDs immediately. The cluster mechanism ensures that once those IDs are acknowledged, they get folded into a contiguous block of the final number line. The cost of that finalization step is the same regardless of how many IDs were created.
89
+ * **Eager Finalization:** If a session already has an allocated cluster with free slots, **new IDs can skip the placeholder stage entirely** – they are *eagerly assigned final IDs at creation*. In the cluster chain, such IDs are marked as final from the start (the compressor knows “ID number N for this session will ultimately be final X” without waiting). This greatly improves efficiency for bursty ID generation, because after one round trip to obtain a cluster, the next many IDs incur no additional coordination cost.
90
90
 
91
- UUID generation is O(1).
91
+ **Example – ID Generation Workflow:**
92
+ Consider a simple scenario with two client sessions (A and B) to illustrate local and final ID assignment:
92
93
 
93
- #### Decompression/recompression
94
+ * **Step 1:** Session A creates its first ID by calling `generateCompressedId()`. No cluster exists yet for A, so the ID is assigned a session-local ID **-1**. Similarly, A creates a second ID, getting **-2**. These negative IDs are unique within A, but session B could concurrently also create an ID -1 in its own space (no conflict because B is a different session).
94
95
 
95
- Converting an allocated ID into its UUID form (decompression) and the reverse (recompression) are both O(log<sub>n</sub>) in the number of IDs created by the originating session.
96
+ * **Step 2:** Session A prepares an op to announce its new IDs. The Fluid runtime calls `takeNextCreationRange()` on A’s compressor, which returns a range describing the two new IDs (session A’s ID, first generation count = 1, count = 2, etc.). This is included in A’s op and sent to the server. The server sequences this op (ensuring all clients process it in the same order).
96
97
 
97
- #### Clustering
98
+ * **Step 3:** When A’s op is sequenced, each client (including A itself) calls `finalizeCreationRange()` with that range. The ID Compressor on each side allocates A’s **first cluster**. Suppose the implementation chooses a cluster of size, say, 5 for A. If no final IDs were yet allocated in the document, A’s cluster might start at final ID **0**. The cluster is recorded as belonging to session A with base final ID 0 and capacity 5. Now the two IDs are finalized: A’s **-1 → 0** and **-2 → 1**. The cluster still has 3 unused slots (final IDs 2, 3, 4) reserved for future IDs from A.
98
99
 
99
- The sequential allocation approach allows the system to implement a clustering scheme.
100
+ * **Step 4:** Session A generates a third ID. Now A’s compressor finds an existing cluster with free space (3 slots remain). Therefore, it **eagerly assigns** the next final ID: the new ID is given final ID **2** immediately (instead of a negative number). Likewise, a fourth and fifth ID from A would get final IDs **3** and **4**. At this point, A’s cluster has been filled up (all 5 of its reserved IDs 0–4 are used). Session A’s compressor has not needed to send another range yet because these were already reserved in the first cluster.
100
101
 
101
- As allocators across the network create IDs, they reserve blocks of positive ID space (clusters) for that allocator. The positive integer space is sharded across different clients, and session space IDs are mapped into these clusters.
102
+ * **Step 5:** Meanwhile, suppose Session B also generated some IDs and got its first cluster when its op was sequenced. Perhaps B’s first cluster starts at final ID **5** with capacity 5 (covering 5–9). If B created one ID and sent it, B’s **-1** would map to final **5**, and B now has final IDs 6–9 in reserve for more creations.
102
103
 
103
- Clusters are efficient to store, requiring only two integers: the base positive integer in the cluster and the count of reserved IDs in that cluster.
104
+ * **Step 6:** Session A now generates a sixth ID. A’s first cluster is exhausted, and because another session (B) has since allocated the next chunk of the ID space (up through 9), A cannot extend its previous cluster contiguously. As a result, A’s sixth ID is assigned a **session-local ID -6** (negative) at creation. The runtime sends another creation range for this one new ID in a subsequent op.
104
105
 
105
- #### Normalization
106
+ * **Step 7:** When that op is sequenced, A’s compressor finalizes the new range. Since A’s previous cluster (0–4) was not the last in the global ordering (B had 5–9), the compressor allocates **a new cluster** for A. This second cluster might start at final ID **10** (the next available after 9). The cluster’s capacity could again be, say, 5 (covering 10–14). Now A’s ID **-6** is finalized to **10**. With that cluster in place, if A generates more IDs (7th, 8th, etc.), up to four of them can be assigned final IDs 11, 12, 13, 14 immediately without going local.
106
107
 
107
- Normalization is O(log<sub>n</sub>) in the number of IDs created by the originating session, as it requires a simple binary search on the clusters owned by that session.
108
+ This example shows how session A’s **cluster chain** consists of two clusters: one spanning final IDs 0–4 and another spanning 10–14. Session B has its own cluster (5–9). Each session’s local IDs start at -1 for their first cluster, and continue negatively (A used -1 through -5 for the first 5 IDs, then -6 for the next cluster’s first ID, and so on). In all cases, the **baseLocalId of the first cluster is -1** (by design of `finalizeCreationRange`, which will error if it isn’t). The mapping within each cluster is straightforward linear offset, and no two sessions’ final ID ranges overlap. By using negative IDs, the system allowed each session to act independently when it ran out of reserved space, and by using generous cluster reservations and expansions, it minimized how often those independent actions are needed.
108
109
 
109
- <!-- AUTO-GENERATED-CONTENT:START (README_FOOTER) -->
110
+ ## API Overview and Usage
110
111
 
111
- <!-- prettier-ignore-start -->
112
- <!-- NOTE: This section is automatically generated using @fluid-tools/markdown-magic. Do not update these generated contents directly. -->
113
-
114
- ## Minimum Client Requirements
115
-
116
- These are the platform requirements for the current version of Fluid Framework Client Packages.
117
- These requirements err on the side of being too strict since within a major version they can be relaxed over time, but not made stricter.
118
- For Long Term Support (LTS) versions this can require supporting these platforms for several years.
112
+ The ID Compressor provides methods to generate IDs and to convert between the various representations (session space, op space, stable IDs):
119
113
 
120
- It is likely that other configurations will work, but they are not supported: if they stop working, we do not consider that a bug.
121
- If you would benefit from support for something not listed here, file an issue and the product team will evaluate your request.
122
- When making such a request please include if the configuration already works (and thus the request is just that it becomes officially supported), or if changes are required to get it working.
114
+ * **`generateCompressedId()`**: Creates a new stable ID (UUID) and returns its compressed form in **session space**. The returned `SessionSpaceCompressedId` is either a negative number (if no final ID was immediately available) or a non-negative final ID (if allocated from an existing cluster) as described above. In either case, the compressor internally generates a new UUID for it. This call is **synchronous** and always succeeds in producing a unique ID for the session. The stable UUID can be retrieved at any time via `decompress()`. Generation is O(1) in time.
123
115
 
124
- ### Supported Runtimes
116
+ * **`takeNextCreationRange()`**: Gathers all IDs generated since the last range was taken, packaging them into an `IdCreationRange` object. This includes the session’s ID (sessionId) and, if any IDs were created, a description of the first ID’s generation index, how many IDs (`count`), the requested cluster size, and the spans of those IDs that were session-local. The Fluid runtime uses this method internally when preparing an outbound operation that includes newly created IDs. Application developers **do not need to call this directly** – it is handled by the framework when submitting ops. The ranges **must** be sent in the order they are taken; calling this will also increment an internal marker to enforce that ordering.
125
117
 
126
- - NodeJs ^20.10.0 except that we will drop support for it [when NodeJs 20 loses its upstream support on 2026-04-30](https://github.com/nodejs/release#release-schedule), and will support a newer LTS version of NodeJS (22) at least 1 year before 20 is end-of-life. This same policy applies to NodeJS 22 when it is end of life (2027-04-30).
127
- - Running Fluid in a Node.js environment with the `--no-experimental-fetch` flag is not supported.
128
- - Modern browsers supporting the es2022 standard library: in response to asks we can add explicit support for using babel to polyfill to target specific standards or runtimes (meaning we can avoid/remove use of things that don't polyfill robustly, but otherwise target modern standards).
118
+ * **`finalizeCreationRange(range: IdCreationRange)`**: Incorporates a range of IDs (which could be from a remote session or the local session) into the compressor’s state as finalized. This call will allocate or update clusters for the specified session. If it’s the first time that sessionId is seen, a new cluster chain is started (and the base local ID of that first cluster must align to -1 as checked internally). If the session already has an existing cluster chain, the compressor will attempt to **expand the last cluster** if possible or create a new cluster. Specifically, if the range being finalized picks up exactly where the session’s last cluster left off *and* that last cluster is also the highest final ID in the entire system, the compressor will extend the cluster’s capacity to accommodate the new IDs (and possibly some additional reserved capacity). This is the **cluster expansion** optimization: by enlarging the last cluster when safe, the compressor avoids fragmenting the ID space and allows the session to continue with a single contiguous range, reducing the need for future local IDs. If the last cluster cannot be expanded (e.g., another session’s cluster comes after it globally), a new cluster is allocated for this session’s new range. After finalization, the mapping of those session-local IDs to final IDs is fixed and consistent across all clients. This method runs in O(1) time.
129
119
 
130
- ### Supported Tools
120
+ * **`decompress(id: SessionSpaceCompressedId): StableId`**: Converts a compressed ID (in session space) back into the 128-bit stable UUID string. This works for any compressed ID that the compressor knows about – whether it’s a local negative ID or a final ID. If the ID is session-local and negative (not final), the compressor still knows the stable UUID because it was generated at creation time, and will return it by computing a simple offset from the base session UUID. If the ID is final, it likewise returns the stable UUID by finding the containing cluster and computing the offset. Decompression is O(log max(*n*, *m*)) where *n* is the number of clusters in final space and *m* is the number of "runs" of local IDs in the local session (contiguous negative IDs generated with no eager finals). The former is the time when decompressing a final space ID, and the latter for a negative ID (it would be O(1) but for error checking querying the normalizer).
131
121
 
132
- - TypeScript 5.4:
133
- - All [`strict`](https://www.typescriptlang.org/tsconfig) options are supported.
134
- - [`strictNullChecks`](https://www.typescriptlang.org/tsconfig) is required.
135
- - [Configuration options deprecated in 5.0](https://github.com/microsoft/TypeScript/issues/51909) are not supported.
136
- - `exactOptionalPropertyTypes` is currently not fully supported.
137
- If used, narrowing members of Fluid Framework types types using `in`, `Reflect.has`, `Object.hasOwn` or `Object.prototype.hasOwnProperty` should be avoided as they may incorrectly exclude `undefined` from the possible values in some cases.
138
- - [webpack](https://webpack.js.org/) 5
139
- - We are not intending to be prescriptive about what bundler to use.
140
- Other bundlers which can handle ES Modules should work, but webpack is the only one we actively test.
122
+ * **`recompress(uncompressed: StableId): SessionSpaceCompressedId`** and **`tryRecompress(uncompressed: StableId): SessionSpaceCompressedId | undefined`**: These APIs perform the reverse of decompression – given a stable UUID, they attempt to find its compressed ID representation for this compressor’s session space. If the UUID was **originally generated by this compressor** (or is otherwise recognized in the compressor’s cluster records), this will return the corresponding session-space ID. For example, if you pass in a stable ID that this session created, `recompress` will return the same negative ID (or final ID) that was originally issued for it. If you pass a stable ID created by another session that has since been finalized, it will return the final ID (since in this session’s space, that is the only form of that ID). If the stable ID is not known to the compressor at all (meaning it wasn’t generated by any session in the document’s history that this compressor is aware of), `tryRecompress` will return `undefined` and `recompress` will throw an error. This situation simply indicates the UUID was never introduced via this compression scheme – for instance, it might be a completely external ID or from a different document.
141
123
 
142
- ### Module Resolution
124
+ * **`normalizeToOpSpace(id: SessionSpaceCompressedId): OpSpaceCompressedId`**: Converts a session-space ID into op space, for use in an outgoing op or other serialized form. If the given ID is from the local session and has a final ID (either it was eagerly finalized or has since been finalized), this will yield that final ID. If it’s from the local session and still unfinalized (a negative ID with no assigned final yet), it will remain as a negative but will be tagged as an `OpSpaceCompressedId` type (essentially the same numeric value). If the ID passed in is actually already a final ID (including any non-local session-space ID), it simply returns it unchanged (already normalized). This method will throw an error if the ID is invalid or unknown to the compressor (which should not happen if the ID came from a valid source).
143
125
 
144
- [`Node16`, `NodeNext`, or `Bundler`](https://www.typescriptlang.org/tsconfig#moduleResolution) resolution should be used with TypeScript compilerOptions to follow the [Node.js v12+ ESM Resolution and Loading algorithm](https://nodejs.github.io/nodejs.dev/en/api/v20/esm/#resolution-and-loading-algorithm).
145
- Node10 resolution is not supported as it does not support Fluid Framework's API structuring pattern that is used to distinguish stable APIs from those that are in development.
126
+ * **`normalizeToSessionSpace(id: OpSpaceCompressedId, originSessionId: SessionId): SessionSpaceCompressedId`**: Converts an op-space ID plus its originating session into this compressor’s session space form. This is used when receiving an op or reading persisted data. The originSessionId tells the compressor whose context the ID was created in. The logic is as follows: If the `id` is a final ID (non-negative), the compressor checks if it falls into any cluster of the local session. If it belongs to this local session’s own cluster chain (meaning the ID was actually created by this session and finalized), the compressor will see if that ID had a local form. If it did (normalizer knows it was created as local), it returns the local negative ID. If it didn’t (it was an eagerly assigned final for this session), it will just return the same final ID as a SessionSpaceCompressedId. If the final ID does not belong to the local session’s clusters, the compressor checks that it is within the range of allocated finals in the entire document (to ensure it’s not beyond any known ID). If it’s known and belongs to some other session, it returns the ID as-is (since from this session’s perspective, that final ID is the only form of the ID; there is no local negative for it in this compressor). On the other hand, if the `id` is a negative number (meaning the op carried a session-local ID that wasn’t finalized at the time of send), then if the originSessionId matches this compressor’s session, it simply verifies the ID is one of our own (we generated it) and returns it. If the originSessionId is some other session, the compressor looks up that session’s cluster chain and converts the remote local ID to its final ID if possible. If the remote local ID isn’t yet finalized in our state, it means we haven’t processed the creation op for it yet (this can happen if ops were received out of order), and `normalizeToSessionSpace` will throw an error for an “unknown op space ID”. In practice, the Fluid runtime ensures that creation ranges are processed before any ops that reference those IDs, so such an out-of-order scenario is typically avoided.
146
127
 
147
- ### Module Formats
128
+ * **`serialize(withSession: boolean)`**: Exports the compressor’s state to a binary/blittable format for storage. If `withSession` is true, it includes the current session’s unfinalized state (any locally generated IDs not yet finalized, and the session’s ability to continue generating sequential UUIDs). This is used for offline storage (e.g., snapshotting a local client’s state) so that the exact sequence can resume on reload. If `withSession` is false, it serializes only the finalized state (all known clusters and sessions, but no info about ongoing local generation). This is suitable for persisting in summaries that go to the service, as it does not tie the summary to a particular client’s in-progress state. On deserialization, a compressor can be reconstructed from the serialized data. Typically the Fluid container’s runtime handles serialization/deserialization of the ID Compressor as part of summarizing or loading a document. Application developers don’t usually call this directly, but maintainers should ensure any changes to compressor internals maintain compatibility with the serialized format (versioned via an internal version number).
148
129
 
149
- - ES Modules:
150
- ES Modules are the preferred way to consume our client packages (including in NodeJs) and consuming our client packages from ES Modules is fully supported.
151
- - CommonJs:
152
- Consuming our client packages as CommonJs is supported only in NodeJS and only for the cases listed below.
153
- This is done to accommodate some workflows without good ES Module support.
154
- If you have a workflow you would like included in this list, file an issue.
155
- Once this list of workflows motivating CommonJS support is empty, we may drop support for CommonJS one year after notice of the change is posted here.
130
+ ## Performance and Optimizations
156
131
 
157
- - Testing with Jest (which lacks [stable ESM support](https://jestjs.io/docs/ecmascript-modules) due to [unstable APIs in NodeJs](https://github.com/nodejs/node/issues/37648))
132
+ The ID Compressor is designed for high performance in the collaborative runtime:
158
133
 
159
- ## Contribution Guidelines
134
+ * **Generation Throughput:** Creating new IDs (`generateCompressedId`) is an O(1) operation – it just increments a counter and does a couple of checks for cluster availability. There is no contention or waiting; even if an ID ends up being session-local, it’s generated instantly without needing consensus.
160
135
 
161
- There are many ways to [contribute](https://github.com/microsoft/FluidFramework/blob/main/CONTRIBUTING.md) to Fluid.
136
+ * **Compression/Decompression:** Converting between stable IDs and compressed IDs relies on searching through a session’s clusters or local-ID ranges. Because clusters are stored in sorted order and typically few in number, and because local-ID ranges are stored in a compact run-length format, these conversions are very fast. The worst-case complexity is O(log *n*), where *n* is the number of clusters in final space.
162
137
 
163
- - Participate in Q&A in our [GitHub Discussions](https://github.com/microsoft/FluidFramework/discussions).
164
- - [Submit bugs](https://github.com/microsoft/FluidFramework/issues) and help us verify fixes as they are checked in.
165
- - Review the [source code changes](https://github.com/microsoft/FluidFramework/pulls).
166
- - [Contribute bug fixes](https://github.com/microsoft/FluidFramework/blob/main/CONTRIBUTING.md).
138
+ * **Memory Efficiency:** Each **cluster** is stored as a tuple of four integers plus the session reference: the base final ID, the capacity, the current count, and the base local ID (the local ID associated with the first final ID in the cluster). This means even a very large number of IDs can be represented compactly. For example, 10,000 IDs allocated by one session with default cluster sizes would result in at most 20 cluster entries (depending on how interleaved other sessions’ ops were, there may be substantially fewer). The **SessionSpaceNormalizer** (which tracks which IDs were created as local) stores run-length encoded ranges of generation indices, also using very little memory (two numbers per contiguous local run). The compressor avoids storing full UUIDs for every ID by exploiting the sequential generation: it can store a single base UUID and generate others by offset math.
167
139
 
168
- Detailed instructions for working in the repo can be found in the [Wiki](https://github.com/microsoft/FluidFramework/wiki).
140
+ * **Cluster Reservation and Expansion:** The default behavior is to request a reasonably large cluster on the first allocation (e.g., 512 IDs by default) to amortize the cost of coordination. If a session only generates a few IDs, the cost is just one small cluster. If it generates many, having a large cluster allows most IDs to be finalized eagerly. The **cluster expansion** optimization further improves efficiency: when a session is actively producing IDs and remains at the forefront of the global ID space (its cluster is last in the global ordering), finalizing a new range will simply extend that cluster’s capacity rather than start a new one. This means the session can continue using final IDs without interruption. Only when another session “overtakes” by allocating IDs beyond the end of your cluster would you need a new cluster and thus incur another local ID. In the best case (one session generating all IDs or generating the most IDs), that one session might get away with using a single expanding cluster for a very long time. In typical multi-session scenarios, each session will have a modest number of clusters that correlates with how often its ID generation interleaves with others. Overall, this strategy ensures near-optimal use of the numeric space and minimizes the overhead of temporary IDs.
169
141
 
170
- This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
171
- For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
142
+ * **Session Space Normalization Cost:** The SessionSpaceNormalizer structure helps determine if a given ID index was allocated as local or final. It performs containment checks with binary search in a sorted list of ranges, which is O(log *m*) where *m* is the number of local-ID ranges in the session. In practice, *m* is the number of times the session had to fall back to local ID generation – essentially the count of cluster boundaries. With large cluster sizes and expansions, *m* is expected to remain very small (often one or just a few) for most sessions. Thus, checking or normalizing an ID’s form is effectively constant-time in common cases. The normalizer is only updated when a new local ID is generated (simple append/merge at the end of the range list, O(1)).
172
143
 
173
- This project may contain Microsoft trademarks or logos for Microsoft projects, products, or services.
174
- Use of these trademarks or logos must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/trademarks).
175
- Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
144
+ ## Appendix: Session Space Normalizer (Detailed)
176
145
 
177
- ## Help
146
+ The **SessionSpaceNormalizer** is an internal helper that records which IDs a session created in local form. Its purpose is to answer the question: “Given the *n*-th ID generated by this session, was it originally created as a local (negative) ID or as a final (positive) ID?” This is crucial for the compressor to correctly normalize IDs between session space and op space. For example, if the local session generated an ID and gave it a final number immediately (eager final), then when converting that ID to session space later, we should leave it as final (since there was never a negative form). Conversely, if the ID was created as a negative, we should represent it as such in session space even after it has a final ID.
178
147
 
179
- Not finding what you're looking for in this README? Check out [fluidframework.com](https://fluidframework.com/docs/).
148
+ **How it works:** The normalizer maintains a sorted list of ranges (intervals) of **generation indices** that were allocated as local IDs. A generation index is essentially a counter (starting at 1 for the first ID generated, 2 for the second, and so on). When the compressor generates a new ID and determines it must be a local ID, the normalizer’s `addLocalRange()` is called to record that index (or extend the latest range). If multiple local IDs are created in succession, they form a contiguous range of generation indices. If a final ID is created, that index is simply not recorded as local. Thus, the normalizer ends up with something like: “IDs 1-2 were local, 3-5 were final, 6 was local, 7-9 final, 10 was local, etc.” internally.
180
149
 
181
- Still not finding what you're looking for? Please [file an issue](https://github.com/microsoft/FluidFramework/wiki/Submitting-Bugs-and-Feature-Requests).
150
+ For example, referring to the earlier scenario for session A:
182
151
 
183
- Thank you!
152
+ * Generation indices 1 and 2 (IDs -1 and -2) were local, so the normalizer holds range \[1, count=2].
153
+ * Indices 3,4,5 were final (eager, cluster had space), so they are *not* in any local range.
154
+ * Index 6 was local (when cluster1 exhausted), so a new range \[6, count=1] is added.
155
+ * Indices 7,8,9 were final (cluster2 space), so not in local ranges.
156
+ * Index 10 was local (cluster2 exhausted, awaiting cluster3), so a new range \[10, count=1].
184
157
 
185
- ## Trademark
158
+ The normalizer would thus store something like: {(1,2), (6,1), (10,1)} as the local ranges (using base index and count). From this, one can deduce any index’s form.
186
159
 
187
- This project may contain Microsoft trademarks or logos for Microsoft projects, products, or services.
160
+ Concretely, if asked “was the 5th ID local?”, we see 5 is not in any recorded range, meaning it was not allocated as local (it was final). If asked about 6th, 6 falls in the \[6,1] range, so yes, that was local. This information is used when normalizing to session space: e.g., if we have a final ID that corresponds to generation 5 of our session, the compressor checks normalizer – seeing 5 was not a local allocation, it knows the ID had no negative form, so it stays as final in session space. If it was generation 6, normalizer says that was local, so in our session space we prefer the local representation (the negative number).
188
161
 
189
- Use of these trademarks or logos must follow Microsoft's [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
162
+ **Data structure:** SessionSpaceNormalizer uses an append-only sorted map of ranges keyed by the starting generation index. It merges consecutive ranges for efficiency (if two local IDs are created back-to-back, it merges them into one range). Querying if an ID was local is a matter of finding the range at or immediately before that index and checking if the index falls within it – a binary search operation. It can also retrieve all local ranges within a given span of indices, which is used when forming the `localIdRanges` field for `IdCreationRange` (to inform other clients exactly which IDs in the range were local). These operations are fast due to the small number of ranges and binary search.
190
163
 
191
- Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
164
+ **Performance:** As noted, the normalizer’s operations are O(log *m*) for queries, where *m* is the number of disjoint local ranges. In practice *m* is very small. Every time the compressor switches from generating final IDs to a local ID (i.e., when a cluster runs out), a new range might be added. But with large clusters and cluster expansion, this might happen rarely. Even in worst-case scenarios with many interleavings, the number of ranges will be far less than the number of IDs. Memory usage is minimal (two integers per range). The normalizer does not impose any significant overhead and does not need to be reset or pruned during normal operation – once an ID is marked as local, that fact remains relevant for the life of the compressor.
192
165
 
193
- <!-- prettier-ignore-end -->
194
-
195
- <!-- AUTO-GENERATED-CONTENT:END -->
166
+ In summary, the SessionSpaceNormalizer ensures that the **temporal aspect** of ID creation (whether an ID had to be a placeholder or not) is remembered. This guarantees that the compressor can consistently interpret and present IDs in the correct form (local vs final) to the application and for serialization, maintaining the invariants of ID stability and equivalence across the system.
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export declare const pkgName = "@fluidframework/id-compressor";
8
- export declare const pkgVersion = "2.41.0-338401";
8
+ export declare const pkgVersion = "2.41.0";
9
9
  //# sourceMappingURL=packageVersion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.d.ts","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,OAAO,kCAAkC,CAAC;AACvD,eAAO,MAAM,UAAU,kBAAkB,CAAC"}
1
+ {"version":3,"file":"packageVersion.d.ts","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,OAAO,kCAAkC,CAAC;AACvD,eAAO,MAAM,UAAU,WAAW,CAAC"}
@@ -8,5 +8,5 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.pkgVersion = exports.pkgName = void 0;
10
10
  exports.pkgName = "@fluidframework/id-compressor";
11
- exports.pkgVersion = "2.41.0-338401";
11
+ exports.pkgVersion = "2.41.0";
12
12
  //# sourceMappingURL=packageVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEU,QAAA,OAAO,GAAG,+BAA+B,CAAC;AAC1C,QAAA,UAAU,GAAG,eAAe,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/id-compressor\";\nexport const pkgVersion = \"2.41.0-338401\";\n"]}
1
+ {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEU,QAAA,OAAO,GAAG,+BAA+B,CAAC;AAC1C,QAAA,UAAU,GAAG,QAAQ,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/id-compressor\";\nexport const pkgVersion = \"2.41.0\";\n"]}
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export declare const pkgName = "@fluidframework/id-compressor";
8
- export declare const pkgVersion = "2.41.0-338401";
8
+ export declare const pkgVersion = "2.41.0";
9
9
  //# sourceMappingURL=packageVersion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.d.ts","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,OAAO,kCAAkC,CAAC;AACvD,eAAO,MAAM,UAAU,kBAAkB,CAAC"}
1
+ {"version":3,"file":"packageVersion.d.ts","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,OAAO,kCAAkC,CAAC;AACvD,eAAO,MAAM,UAAU,WAAW,CAAC"}
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export const pkgName = "@fluidframework/id-compressor";
8
- export const pkgVersion = "2.41.0-338401";
8
+ export const pkgVersion = "2.41.0";
9
9
  //# sourceMappingURL=packageVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,+BAA+B,CAAC;AACvD,MAAM,CAAC,MAAM,UAAU,GAAG,eAAe,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/id-compressor\";\nexport const pkgVersion = \"2.41.0-338401\";\n"]}
1
+ {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,+BAA+B,CAAC;AACvD,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/id-compressor\";\nexport const pkgVersion = \"2.41.0\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/id-compressor",
3
- "version": "2.41.0-338401",
3
+ "version": "2.41.0",
4
4
  "description": "ID compressor",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -81,23 +81,23 @@
81
81
  "temp-directory": "nyc/.nyc_output"
82
82
  },
83
83
  "dependencies": {
84
- "@fluid-internal/client-utils": "2.41.0-338401",
85
- "@fluidframework/core-interfaces": "2.41.0-338401",
86
- "@fluidframework/core-utils": "2.41.0-338401",
87
- "@fluidframework/telemetry-utils": "2.41.0-338401",
84
+ "@fluid-internal/client-utils": "~2.41.0",
85
+ "@fluidframework/core-interfaces": "~2.41.0",
86
+ "@fluidframework/core-utils": "~2.41.0",
87
+ "@fluidframework/telemetry-utils": "~2.41.0",
88
88
  "@tylerbu/sorted-btree-es6": "^1.8.0",
89
89
  "uuid": "^9.0.0"
90
90
  },
91
91
  "devDependencies": {
92
92
  "@arethetypeswrong/cli": "^0.17.1",
93
93
  "@biomejs/biome": "~1.9.3",
94
- "@fluid-internal/mocha-test-setup": "2.41.0-338401",
95
- "@fluid-private/stochastic-test-utils": "2.41.0-338401",
94
+ "@fluid-internal/mocha-test-setup": "~2.41.0",
95
+ "@fluid-private/stochastic-test-utils": "~2.41.0",
96
96
  "@fluid-tools/benchmark": "^0.51.0",
97
97
  "@fluid-tools/build-cli": "^0.55.0",
98
98
  "@fluidframework/build-common": "^2.0.3",
99
99
  "@fluidframework/build-tools": "^0.55.0",
100
- "@fluidframework/eslint-config-fluid": "^5.7.3",
100
+ "@fluidframework/eslint-config-fluid": "^5.7.4",
101
101
  "@fluidframework/id-compressor-previous": "npm:@fluidframework/id-compressor@2.40.0",
102
102
  "@microsoft/api-extractor": "7.52.8",
103
103
  "@types/mocha": "^10.0.10",
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/id-compressor";
9
- export const pkgVersion = "2.41.0-338401";
9
+ export const pkgVersion = "2.41.0";