@rapidmx/mapi-plugin 1.0.0-beta.3 → 1.0.0-beta.5

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.
Files changed (154) hide show
  1. package/README.md +4 -4
  2. package/dist/lib/BaseMapiEmsmdbRoute.js +229 -37
  3. package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -1
  4. package/dist/lib/MapiSessionManager.js +404 -48
  5. package/dist/lib/MapiSessionManager.js.map +1 -1
  6. package/dist/lib/RegexPatternUtils.js +30 -0
  7. package/dist/lib/RegexPatternUtils.js.map +1 -0
  8. package/dist/lib/RopDispatcher.js +130 -19
  9. package/dist/lib/RopDispatcher.js.map +1 -1
  10. package/dist/lib/codec/AppointmentRecurrence.js +22 -21
  11. package/dist/lib/codec/AppointmentRecurrence.js.map +1 -1
  12. package/dist/lib/codec/BufferCursor.js +41 -12
  13. package/dist/lib/codec/BufferCursor.js.map +1 -1
  14. package/dist/lib/codec/GlobalObjectId.js +26 -20
  15. package/dist/lib/codec/GlobalObjectId.js.map +1 -1
  16. package/dist/lib/codec/MapiTimeZone.js +43 -10
  17. package/dist/lib/codec/MapiTimeZone.js.map +1 -1
  18. package/dist/lib/codec/PropertyValue.js +28 -2
  19. package/dist/lib/codec/PropertyValue.js.map +1 -1
  20. package/dist/lib/codec/RopBuffer.js +15 -3
  21. package/dist/lib/codec/RopBuffer.js.map +1 -1
  22. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +2 -1
  23. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -1
  24. package/dist/lib/nspi/NspiCodec.js +5 -5
  25. package/dist/lib/nspi/NspiCodec.js.map +1 -1
  26. package/dist/lib/nspi/NspiGetMatchesHandler.js +14 -14
  27. package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -1
  28. package/dist/lib/rop/AddressList.js +109 -0
  29. package/dist/lib/rop/AddressList.js.map +1 -0
  30. package/dist/lib/rop/CalendarEventTarget.js +7 -4
  31. package/dist/lib/rop/CalendarEventTarget.js.map +1 -1
  32. package/dist/lib/rop/ContactTarget.js +2 -6
  33. package/dist/lib/rop/ContactTarget.js.map +1 -1
  34. package/dist/lib/rop/ContentsTable.js +60 -0
  35. package/dist/lib/rop/ContentsTable.js.map +1 -0
  36. package/dist/lib/rop/ExecuteBudget.js +82 -0
  37. package/dist/lib/rop/ExecuteBudget.js.map +1 -0
  38. package/dist/lib/rop/FastTransferStream.js +99 -2
  39. package/dist/lib/rop/FastTransferStream.js.map +1 -1
  40. package/dist/lib/rop/FolderTarget.js +13 -6
  41. package/dist/lib/rop/FolderTarget.js.map +1 -1
  42. package/dist/lib/rop/HandleDataCache.js +412 -0
  43. package/dist/lib/rop/HandleDataCache.js.map +1 -0
  44. package/dist/lib/rop/MeetingMessageClassHandler.js +194 -48
  45. package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -1
  46. package/dist/lib/rop/MessageBodyStream.js +33 -7
  47. package/dist/lib/rop/MessageBodyStream.js.map +1 -1
  48. package/dist/lib/rop/MessageTarget.js +17 -4
  49. package/dist/lib/rop/MessageTarget.js.map +1 -1
  50. package/dist/lib/rop/NamedPropertyRegistry.js +8 -5
  51. package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -1
  52. package/dist/lib/rop/PropertyResolvers.js +3 -1
  53. package/dist/lib/rop/PropertyResolvers.js.map +1 -1
  54. package/dist/lib/rop/RepoPaging.js +54 -0
  55. package/dist/lib/rop/RepoPaging.js.map +1 -0
  56. package/dist/lib/rop/RestapiRules.js +52 -0
  57. package/dist/lib/rop/RestapiRules.js.map +1 -0
  58. package/dist/lib/rop/RopCreateMessageHandler.js +4 -2
  59. package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -1
  60. package/dist/lib/rop/RopDeleteFolderHandler.js +118 -77
  61. package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -1
  62. package/dist/lib/rop/RopDeleteMessagesHandler.js +26 -1
  63. package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -1
  64. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +14 -14
  65. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -1
  66. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +13 -14
  67. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -1
  68. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +33 -32
  69. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -1
  70. package/dist/lib/rop/RopGetContentsTableHandler.js +17 -38
  71. package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -1
  72. package/dist/lib/rop/RopGetHierarchyTableHandler.js +4 -2
  73. package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -1
  74. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +6 -8
  75. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -1
  76. package/dist/lib/rop/RopHandler.js +5 -1
  77. package/dist/lib/rop/RopHandler.js.map +1 -1
  78. package/dist/lib/rop/RopLogonHandler.js +5 -2
  79. package/dist/lib/rop/RopLogonHandler.js.map +1 -1
  80. package/dist/lib/rop/RopOpenFolderHandler.js +3 -1
  81. package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -1
  82. package/dist/lib/rop/RopOpenMessageHandler.js +3 -1
  83. package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -1
  84. package/dist/lib/rop/RopOpenStreamHandler.js +11 -8
  85. package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -1
  86. package/dist/lib/rop/RopQueryRowsHandler.js +31 -5
  87. package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -1
  88. package/dist/lib/rop/RopReadStreamHandler.js +22 -4
  89. package/dist/lib/rop/RopReadStreamHandler.js.map +1 -1
  90. package/dist/lib/rop/RopReleaseHandler.js +4 -1
  91. package/dist/lib/rop/RopReleaseHandler.js.map +1 -1
  92. package/dist/lib/rop/RopSaveChangesMessageHandler.js +66 -7
  93. package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -1
  94. package/dist/lib/rop/RopSetColumnsHandler.js +6 -8
  95. package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -1
  96. package/dist/lib/rop/RopSetPropertiesHandler.js +23 -12
  97. package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -1
  98. package/dist/lib/rop/RopSubmitMessageHandler.js +148 -120
  99. package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -1
  100. package/dist/lib/rop/RopWriteStreamHandler.js +37 -10
  101. package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -1
  102. package/dist/lib/rop/TaskTarget.js +2 -6
  103. package/dist/lib/rop/TaskTarget.js.map +1 -1
  104. package/dist/lib/rop/TransportSend.js +16 -0
  105. package/dist/lib/rop/TransportSend.js.map +1 -0
  106. package/dist/lib/sql/MapiEmsmdbRouteSQL.js +2 -1
  107. package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -1
  108. package/dist/types/BaseMapiEmsmdbRoute.d.ts +59 -2
  109. package/dist/types/MapiSessionManager.d.ts +218 -63
  110. package/dist/types/RegexPatternUtils.d.ts +14 -0
  111. package/dist/types/RopDispatcher.d.ts +47 -11
  112. package/dist/types/codec/BufferCursor.d.ts +14 -0
  113. package/dist/types/codec/GlobalObjectId.d.ts +9 -5
  114. package/dist/types/codec/PropertyValue.d.ts +10 -0
  115. package/dist/types/codec/RopBuffer.d.ts +7 -0
  116. package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +1 -0
  117. package/dist/types/nspi/NspiGetMatchesHandler.d.ts +2 -0
  118. package/dist/types/rop/AddressList.d.ts +48 -0
  119. package/dist/types/rop/CalendarEventTarget.d.ts +7 -3
  120. package/dist/types/rop/ContactTarget.d.ts +3 -3
  121. package/dist/types/rop/ContentsTable.d.ts +16 -0
  122. package/dist/types/rop/ExecuteBudget.d.ts +48 -0
  123. package/dist/types/rop/FastTransferStream.d.ts +41 -2
  124. package/dist/types/rop/FolderTarget.d.ts +3 -2
  125. package/dist/types/rop/HandleDataCache.d.ts +145 -0
  126. package/dist/types/rop/MeetingMessageClassHandler.d.ts +23 -29
  127. package/dist/types/rop/MessageBodyStream.d.ts +16 -7
  128. package/dist/types/rop/MessageTarget.d.ts +10 -3
  129. package/dist/types/rop/NamedPropertyRegistry.d.ts +6 -2
  130. package/dist/types/rop/PropertyResolvers.d.ts +1 -1
  131. package/dist/types/rop/RepoPaging.d.ts +30 -0
  132. package/dist/types/rop/RestapiRules.d.ts +32 -0
  133. package/dist/types/rop/RopCreateMessageHandler.d.ts +1 -0
  134. package/dist/types/rop/RopDeleteFolderHandler.d.ts +36 -21
  135. package/dist/types/rop/RopDeleteMessagesHandler.d.ts +7 -0
  136. package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +1 -0
  137. package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +1 -0
  138. package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +10 -11
  139. package/dist/types/rop/RopGetContentsTableHandler.d.ts +7 -13
  140. package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +1 -0
  141. package/dist/types/rop/RopHandler.d.ts +40 -1
  142. package/dist/types/rop/RopOpenFolderHandler.d.ts +1 -0
  143. package/dist/types/rop/RopOpenMessageHandler.d.ts +1 -0
  144. package/dist/types/rop/RopOpenStreamHandler.d.ts +1 -0
  145. package/dist/types/rop/RopQueryRowsHandler.d.ts +3 -0
  146. package/dist/types/rop/RopReadStreamHandler.d.ts +3 -1
  147. package/dist/types/rop/RopReleaseHandler.d.ts +1 -0
  148. package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +16 -1
  149. package/dist/types/rop/RopSubmitMessageHandler.d.ts +33 -23
  150. package/dist/types/rop/RopWriteStreamHandler.d.ts +18 -8
  151. package/dist/types/rop/TaskTarget.d.ts +3 -3
  152. package/dist/types/rop/TransportSend.d.ts +10 -0
  153. package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +1 -0
  154. package/package.json +3 -3
@@ -11,30 +11,43 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
12
12
  // SPDX-License-Identifier: MPL-2.0
13
13
  ///////////////////////////////////////////////////////////////////////////////
14
+ import * as crypto from "crypto";
14
15
  import { ObjectDecorators } from "@rapidrest/core";
15
- import { RedisCache, SimpleEntity } from "@rapidrest/service-core";
16
+ import { DatabaseDecorators, SimpleEntity } from "@rapidrest/service-core";
17
+ import { handleDataCache, handleDataKey, MemoryHandleDataStore, RedisHandleDataStore, sessionHandleDataIndex, writeStreamKey, } from "./rop/HandleDataCache.js";
16
18
  const { Init } = ObjectDecorators;
19
+ const { Redis } = DatabaseDecorators;
17
20
  /** A pragmatic idle-session lifetime. The spec leaves session lifetime to server policy, not a fixed wire
18
21
  * value - a real client re-establishes (`Connect`) transparently whenever its session context has expired. */
19
- const SESSION_TTL_SECONDS = 15 * 60;
22
+ export const SESSION_TTL_SECONDS = 15 * 60;
23
+ /** The longest a session may live, however active it stays. A client simply `Connect`s again afterwards. */
24
+ export const MAX_SESSION_LIFETIME_MS = 24 * 60 * 60 * 1000;
25
+ /** The most sessions one user may hold at once. `Connect` past this ends that user's least recently used session (each
26
+ * `Execute` refreshes its session's place - see `MapiSessionManager.touch`). */
27
+ export const MAX_SESSIONS_PER_USER = 20;
28
+ /** The largest a serialized session may grow. Everything that grows with use is capped on its own (handles by their
29
+ * one-byte index, named properties, MIDs; write streams live outside the session); this is the backstop, and a save
30
+ * past it fails with `MAPI_E_TOO_BIG` rather than storing megabytes every later request has to load. */
31
+ export const MAX_SESSION_BYTES = 4 * 1024 * 1024;
32
+ /** How long an `Execute`'s session lock lives without being renewed. A running `Execute` renews it every
33
+ * `SESSION_LOCK_RENEW_MS`, so only a crashed pod's lock expires. The session save is still compare-and-set, so an
34
+ * expired lock can't corrupt state. */
35
+ export const SESSION_LOCK_TTL_MS = 120 * 1000;
36
+ /** How often a running `Execute` renews its session lock and in-progress marker. */
37
+ export const SESSION_LOCK_RENEW_MS = SESSION_LOCK_TTL_MS / 3;
20
38
  /**
21
39
  * The MAPI/HTTP `Session Context` (`[MS-OXCMAPIHTTP]` §3.1.1.1): everything a `Connect`-established session
22
40
  * needs across subsequent `Execute` requests. Never persisted to a real database - purely an ephemeral,
23
- * TTL-bound cache entry (see `MapiSessionManager` below), so this deliberately extends `SimpleEntity` (just a
24
- * `uid`) rather than this library's own `BaseEntity`, which would add ACL/soft-delete/optimistic-locking
25
- * machinery this object has no use for.
41
+ * TTL-bound store entry (see `MapiSessionManager` below), so this deliberately extends `SimpleEntity` (just a
42
+ * `uid`) rather than this library's own `BaseEntity`.
26
43
  *
27
- * `createdAt` is a plain ISO-8601 string, not a `Date` - `RedisCache`'s Redis-backed path round-trips values
28
- * through `JSON.stringify`/`JSON.parse`, which silently turns a `Date` into a string on the way out without
29
- * reviving it back to a `Date` on the way in; storing it as a string from the start keeps the shape identical
30
- * whether a session happens to be served from the in-memory or the Redis-backed path.
44
+ * `createdAt` is a plain ISO-8601 string, not a `Date`, since sessions round-trip through JSON. `version` is
45
+ * bumped on every successful save and compared on the next one - see `MapiSessionManager.save()`.
31
46
  */
32
47
  export class MapiSessionContext extends SimpleEntity {
33
- /** `mailboxUid`/`userUid` are always known at construction time (the only call site is
34
- * `MapiSessionManager.create()`, which resolves both up front) - required here rather than optional with
35
- * a same-value fallback, which would just be dead code no real caller ever takes the other branch of. */
36
48
  constructor(other) {
37
49
  super(other);
50
+ this.version = 0;
38
51
  this.handles = {};
39
52
  this.nextHandleIndex = 1;
40
53
  this.createdAt = new Date().toISOString();
@@ -44,32 +57,25 @@ export class MapiSessionContext extends SimpleEntity {
44
57
  this.folderIds = {};
45
58
  /** The reverse of `folderIds` (target -> FID) plus a monotonic counter, maintained alongside it by
46
59
  * `FolderTarget.assignOrGetFid`/`RopLogonHandler` so "does this target already have a FID" is an O(1)
47
- * lookup instead of a linear scan of `folderIds` repeated once per table row - see `assignOrGetFid`'s own
48
- * doc comment for why the scan this replaces was a genuine (not just theoretical) quadratic cost across a
49
- * session's lifetime. Small and purely additive to session state (one more string-keyed map of numbers,
50
- * the same order of magnitude as `folderIds` itself), unlike caching whole resolved rows would be. */
60
+ * lookup instead of a linear scan of `folderIds` repeated once per table row. */
51
61
  this.folderTargetIds = {};
52
62
  this.nextFolderId = 1;
53
63
  /** This session's MID assignments, keyed by MID (decimal string), valued `"message:<uid>"` - the message
54
- * analog of `folderIds` above. Unlike `folderIds` (pre-populated by `RopLogon`), a MID only ever comes into
55
- * existence lazily, the first time a `RopQueryRows` row exposes a message's `PidTagMid` column (see
56
- * `MessageTarget.assignOrGetMid`), read back by a later `RopOpenMessage`. */
64
+ * analog of `folderIds` above. A MID only ever comes into existence lazily, the first time a `RopQueryRows`
65
+ * row exposes a message's `PidTagMid` column (see `MessageTarget.assignOrGetMid`), read back by a later
66
+ * `RopOpenMessage`. */
57
67
  this.messageIds = {};
58
- /** The reverse of `messageIds` (target -> MID) plus a monotonic counter - the message analog of
59
- * `folderTargetIds`/`nextFolderId` above, for the identical O(1)-lookup reason. */
68
+ /** The reverse of `messageIds` (target -> MID) plus a monotonic counter. */
60
69
  this.messageTargetIds = {};
61
70
  this.nextMessageId = 1;
71
+ /** The oldest MID still mapped. Past `MAX_MESSAGE_IDS` mapped MIDs the oldest are dropped - see
72
+ * `MessageTarget.assignOrGetMid`. */
73
+ this.firstMessageId = 1;
62
74
  /** This session's `RopGetPropertyIdsFromNames` mapping table (`[MS-OXCPRPT]` §2.2.12), keyed by a JSON
63
75
  * string encoding of the `{guid, kind, lid|name}` `PropertyName` the numeric ID was assigned to - see
64
- * `NamedPropertyRegistry.ts`'s own doc comment for why a JSON string (rather than a delimiter-joined one)
65
- * is the safest key shape here. A real client resolves every named property (almost every Calendar-specific
66
- * one - `PidLidAppointmentStartWhole`, `PidLidBusyStatus`, `PidLidAppointmentRecur`, ...) through this table
67
- * once per session before ever setting/reading it via `RopSetProperties`/`RopGetPropertiesSpecific`. */
76
+ * `NamedPropertyRegistry.ts`. */
68
77
  this.namedProperties = {};
69
- /** The reverse of `namedProperties` (numeric ID -> `PropertyName` JSON key) plus a monotonic counter -
70
- * `NamedPropertyRegistry.ts`'s own doc comment covers why both the O(n) forward scan this replaces and the
71
- * `Math.max(...spread)` id-allocation it used are real problems, not just theoretical ones, at real
72
- * mailbox/session scale. */
78
+ /** The reverse of `namedProperties` (numeric ID -> `PropertyName` JSON key) plus a monotonic counter. */
73
79
  this.namedPropertyIds = {};
74
80
  this.nextNamedPropertyId = 0x8000;
75
81
  this.mailboxUid = other.mailboxUid;
@@ -77,43 +83,393 @@ export class MapiSessionContext extends SimpleEntity {
77
83
  }
78
84
  }
79
85
  /**
80
- * Stores/loads `MapiSessionContext`s, keyed by the opaque session id that also becomes the `MapiContext`
81
- * cookie value. Reuses `@rapidrest/service-core`'s existing generic `RedisCache<T>` (Redis-backed when a
82
- * `cache` datastore is configured, transparent in-memory fallback otherwise) rather than either a bespoke
83
- * Redis client (unlike `PingCommand`'s own throwaway pub/sub client - fine for ephemeral fan-out, not a good
84
- * fit for stateful session data) or the framework's generic cookie/session middleware (built for a whole-app
85
- * `req.session` object, not this protocol's specific handle-table shape). `RedisCache` needs no real
86
- * persisted entity - only a class with a `.name` for its Redis key prefix - so a purely ephemeral,
87
- * never-persisted class like `MapiSessionContext` is a perfectly valid `type` for it.
86
+ * Stores `handle` at `index` with a fresh generation. Whatever previously held that index is released first
87
+ * (see `releaseHandle`), so a stream or cached data belonging to the old handle never attaches to the new one.
88
+ */
89
+ export function assignHandle(session, index, handle) {
90
+ releaseHandle(session, index);
91
+ handle.generation = crypto.randomUUID();
92
+ session.handles[index] = handle;
93
+ return handle;
94
+ }
95
+ /**
96
+ * Releases the handle at `index`: drops its cached data, and releases every write stream opened against it
97
+ * (a stream is meaningless once its draft message is gone, and would otherwise attach to whatever reuses the
98
+ * index next).
99
+ */
100
+ export function releaseHandle(session, index) {
101
+ const handle = session.handles[index];
102
+ if (!handle) {
103
+ return;
104
+ }
105
+ delete session.handles[index];
106
+ handleDataCache.delete(handleDataKey(session.uid, index, handle.generation));
107
+ if (handle.type === "fastTransfer" || handle.type === "stream") {
108
+ const released = releasedHandleData.get(session) ?? [];
109
+ released.push(handle.type === "fastTransfer" ? handleDataKey(session.uid, index, handle.generation) : writeStreamKey(session.uid, index, handle.generation));
110
+ releasedHandleData.set(session, released);
111
+ }
112
+ for (const [key, candidate] of Object.entries(session.handles)) {
113
+ if (candidate.writeTargetHandleIndex === index && candidate.writeTargetGeneration === handle.generation) {
114
+ releaseHandle(session, Number(key));
115
+ }
116
+ }
117
+ }
118
+ /** Session object -> the `HandleDataStore` keys of the handles released on it since the last `takeReleasedHandleData`.
119
+ * Kept beside the session, never in it, so it isn't saved. */
120
+ const releasedHandleData = new WeakMap();
121
+ /** The shared-store keys (FastTransfer streams, write-stream chunks) of every handle released on `session` since the
122
+ * last call, for the route to delete once the session's changes are saved. */
123
+ export function takeReleasedHandleData(session) {
124
+ const released = releasedHandleData.get(session) ?? [];
125
+ releasedHandleData.delete(session);
126
+ return released;
127
+ }
128
+ /** Creates a session at version 0. KEYS: session, version. ARGV: json, ttl. */
129
+ export const CREATE_SCRIPT = `
130
+ redis.call('SETEX', KEYS[1], ARGV[2], ARGV[1])
131
+ redis.call('SETEX', KEYS[2], ARGV[2], '0')
132
+ return 1
133
+ `;
134
+ /** Replaces the session only when its version entry still equals the expected one, without decoding the session.
135
+ * KEYS: session, version. ARGV: expected version, json, ttl. Returns 1 saved, 0 on a version mismatch, -1 when gone. */
136
+ export const COMPARE_AND_SET_SCRIPT = `
137
+ local current = redis.call('GET', KEYS[2])
138
+ if not current or redis.call('EXISTS', KEYS[1]) == 0 then return -1 end
139
+ if tonumber(current) ~= tonumber(ARGV[1]) then return 0 end
140
+ redis.call('SETEX', KEYS[1], ARGV[3], ARGV[2])
141
+ redis.call('SETEX', KEYS[2], ARGV[3], tostring(tonumber(ARGV[1]) + 1))
142
+ return 1
143
+ `;
144
+ /** Adds a session to its user's sorted-set index in one step, so concurrent Connects can't each see room under the
145
+ * cap. KEYS: index. ARGV: session id, created-at score, max, index ttl, session key prefix, session key suffix. The
146
+ * session keys it checks and deletes are built from the ids, so they aren't declared in KEYS (fine on one Redis node,
147
+ * not on a cluster). Returns the ids it ended. */
148
+ export const ADD_TO_USER_INDEX_SCRIPT = `
149
+ local function sessionKey(id) return ARGV[5] .. id .. ARGV[6] end
150
+ for _, id in ipairs(redis.call('ZRANGE', KEYS[1], 0, -1)) do
151
+ if redis.call('EXISTS', sessionKey(id)) == 0 then redis.call('ZREM', KEYS[1], id) end
152
+ end
153
+ local ended = {}
154
+ while redis.call('ZCARD', KEYS[1]) >= tonumber(ARGV[3]) do
155
+ local oldest = redis.call('ZRANGE', KEYS[1], 0, 0)[1]
156
+ redis.call('ZREM', KEYS[1], oldest)
157
+ redis.call('DEL', sessionKey(oldest), sessionKey(oldest) .. '.version')
158
+ table.insert(ended, oldest)
159
+ end
160
+ redis.call('ZADD', KEYS[1], ARGV[2], ARGV[1])
161
+ redis.call('EXPIRE', KEYS[1], ARGV[4])
162
+ return ended
163
+ `;
164
+ /** KEYS: lock. ARGV: token, ttl ms. Returns 1 when acquired. */
165
+ export const ACQUIRE_LOCK_SCRIPT = `
166
+ if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then return 1 end
167
+ return 0
168
+ `;
169
+ /** KEYS: lock. ARGV: token. Deletes the lock only while this token still holds it. */
170
+ export const RELEASE_LOCK_SCRIPT = `
171
+ if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) end
172
+ return 1
173
+ `;
174
+ /** KEYS: lock. ARGV: token, ttl ms. Returns 1 when the token still held the lock and it was extended. */
175
+ export const RENEW_LOCK_SCRIPT = `
176
+ if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end
177
+ return 0
178
+ `;
179
+ /** KEYS: user index. ARGV: score, session id. Updates the score of a member that is still present. */
180
+ export const TOUCH_USER_INDEX_SCRIPT = `
181
+ redis.call('ZADD', KEYS[1], 'XX', ARGV[1], ARGV[2])
182
+ return 1
183
+ `;
184
+ /** A `MapiSessionStore` on a node-redis client. Every read goes to Redis, never a per-process copy, so each
185
+ * replica sees the same session. */
186
+ export class RedisMapiSessionStore {
187
+ constructor(client) {
188
+ this.client = client;
189
+ }
190
+ async get(key) {
191
+ return (await this.client.get(key)) ?? undefined;
192
+ }
193
+ async create(key, value, ttlSeconds) {
194
+ await this.client.eval(CREATE_SCRIPT, { keys: [key, versionKey(key)], arguments: [value, String(ttlSeconds)] });
195
+ }
196
+ async compareAndSet(key, expectedVersion, value, ttlSeconds) {
197
+ const result = Number(await this.client.eval(COMPARE_AND_SET_SCRIPT, {
198
+ keys: [key, versionKey(key)],
199
+ arguments: [String(expectedVersion), value, String(ttlSeconds)],
200
+ }));
201
+ return result === 1 ? "saved" : result === 0 ? "conflict" : "missing";
202
+ }
203
+ async delete(key) {
204
+ await this.client.del([key, versionKey(key)]);
205
+ }
206
+ async addToUserIndex(indexKey, sessionId, createdAtMs, max, ttlSeconds) {
207
+ const ended = await this.client.eval(ADD_TO_USER_INDEX_SCRIPT, {
208
+ keys: [indexKey],
209
+ arguments: [sessionId, String(createdAtMs), String(max), String(ttlSeconds), SESSION_KEY_PREFIX, SESSION_KEY_SUFFIX],
210
+ });
211
+ return ended.map(String);
212
+ }
213
+ async acquireLock(key, token, ttlMs) {
214
+ return Number(await this.client.eval(ACQUIRE_LOCK_SCRIPT, { keys: [key], arguments: [token, String(ttlMs)] })) === 1;
215
+ }
216
+ async releaseLock(key, token) {
217
+ await this.client.eval(RELEASE_LOCK_SCRIPT, { keys: [key], arguments: [token] });
218
+ }
219
+ async renewLock(key, token, ttlMs) {
220
+ return Number(await this.client.eval(RENEW_LOCK_SCRIPT, { keys: [key], arguments: [token, String(ttlMs)] })) === 1;
221
+ }
222
+ async touchUserIndex(indexKey, sessionId, score) {
223
+ await this.client.eval(TOUCH_USER_INDEX_SCRIPT, { keys: [indexKey], arguments: [String(score), sessionId] });
224
+ }
225
+ async getValue(key) {
226
+ return (await this.client.get(key)) ?? undefined;
227
+ }
228
+ async putValue(key, value, ttlSeconds) {
229
+ await this.client.setEx(key, ttlSeconds, value);
230
+ }
231
+ }
232
+ /** A single-process `MapiSessionStore`. Values are stored as strings so every load hands out an independent copy,
233
+ * exactly like the Redis store. No method awaits between reading and writing, so each one is atomic. */
234
+ export class MemoryMapiSessionStore {
235
+ constructor() {
236
+ this.entries = new Map();
237
+ this.indexes = new Map();
238
+ }
239
+ async get(key) {
240
+ return this.read(key);
241
+ }
242
+ async create(key, value, ttlSeconds) {
243
+ this.sweep();
244
+ this.write(key, value, ttlSeconds);
245
+ this.write(versionKey(key), "0", ttlSeconds);
246
+ }
247
+ async compareAndSet(key, expectedVersion, value, ttlSeconds) {
248
+ const version = this.read(versionKey(key));
249
+ if (version === undefined || this.read(key) === undefined) {
250
+ return "missing";
251
+ }
252
+ if (Number(version) !== expectedVersion) {
253
+ return "conflict";
254
+ }
255
+ this.write(key, value, ttlSeconds);
256
+ this.write(versionKey(key), String(expectedVersion + 1), ttlSeconds);
257
+ return "saved";
258
+ }
259
+ async delete(key) {
260
+ this.entries.delete(key);
261
+ this.entries.delete(versionKey(key));
262
+ }
263
+ async addToUserIndex(indexKey, sessionId, createdAtMs, max) {
264
+ const members = this.indexes.get(indexKey) ?? new Map();
265
+ this.indexes.set(indexKey, members);
266
+ for (const id of [...members.keys()]) {
267
+ if (this.read(SESSION_KEY_PREFIX + id + SESSION_KEY_SUFFIX) === undefined) {
268
+ members.delete(id);
269
+ }
270
+ }
271
+ const oldestFirst = [...members.entries()].sort((a, b) => a[1] - b[1]).map(([id]) => id);
272
+ const ended = [];
273
+ while (members.size >= max) {
274
+ const oldest = oldestFirst.shift();
275
+ members.delete(oldest);
276
+ this.entries.delete(SESSION_KEY_PREFIX + oldest + SESSION_KEY_SUFFIX);
277
+ this.entries.delete(versionKey(SESSION_KEY_PREFIX + oldest + SESSION_KEY_SUFFIX));
278
+ ended.push(oldest);
279
+ }
280
+ members.set(sessionId, createdAtMs);
281
+ return ended;
282
+ }
283
+ async acquireLock(key, token, ttlMs) {
284
+ if (this.read(key) !== undefined) {
285
+ return false;
286
+ }
287
+ this.entries.set(key, { value: token, expiresAt: Date.now() + ttlMs });
288
+ return true;
289
+ }
290
+ async releaseLock(key, token) {
291
+ if (this.read(key) === token) {
292
+ this.entries.delete(key);
293
+ }
294
+ }
295
+ async renewLock(key, token, ttlMs) {
296
+ if (this.read(key) !== token) {
297
+ return false;
298
+ }
299
+ this.entries.set(key, { value: token, expiresAt: Date.now() + ttlMs });
300
+ return true;
301
+ }
302
+ async touchUserIndex(indexKey, sessionId, score) {
303
+ const members = this.indexes.get(indexKey);
304
+ if (members?.has(sessionId)) {
305
+ members.set(sessionId, score);
306
+ }
307
+ }
308
+ async getValue(key) {
309
+ return this.read(key);
310
+ }
311
+ async putValue(key, value, ttlSeconds) {
312
+ this.write(key, value, ttlSeconds);
313
+ }
314
+ read(key) {
315
+ const entry = this.entries.get(key);
316
+ if (entry && entry.expiresAt <= Date.now()) {
317
+ this.entries.delete(key);
318
+ return undefined;
319
+ }
320
+ return entry?.value;
321
+ }
322
+ write(key, value, ttlSeconds) {
323
+ this.entries.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
324
+ }
325
+ sweep() {
326
+ const now = Date.now();
327
+ for (const [key, entry] of this.entries) {
328
+ if (entry.expiresAt <= now) {
329
+ this.entries.delete(key);
330
+ }
331
+ }
332
+ }
333
+ }
334
+ // A session's keys put its id in a hash tag, so its JSON, version, lock and last response share one cluster slot.
335
+ const SESSION_KEY_PREFIX = "mapi.session.{";
336
+ const SESSION_KEY_SUFFIX = "}";
337
+ /** The store key of a session's JSON. */
338
+ export function sessionKey(sessionId) {
339
+ return SESSION_KEY_PREFIX + encodeURIComponent(sessionId) + SESSION_KEY_SUFFIX;
340
+ }
341
+ /** The `.last` value marking `requestId` as running. Always serialized the same way, so it can be compared as a string. */
342
+ function inProgressMarker(requestId) {
343
+ return JSON.stringify({ requestId });
344
+ }
345
+ function versionKey(key) {
346
+ return `${key}.version`;
347
+ }
348
+ function userIndexKey(userUid) {
349
+ return `mapi.session.user.${encodeURIComponent(userUid)}`;
350
+ }
351
+ /**
352
+ * Stores/loads `MapiSessionContext`s, keyed by the opaque session id that also becomes the `MapiContext` cookie
353
+ * value.
88
354
  *
89
- * @author Jean-Philippe Steinmetz
355
+ * Every load reads the shared store (Redis when a `cache` datastore is configured). An earlier version used
356
+ * `RedisCache`, whose per-process copy served a session without checking Redis, so with several replicas one pod
357
+ * could run ROPs against handles another pod had already changed. Saves are compare-and-set on `version`: when
358
+ * two requests on the same session overlap, the second save reports `"conflict"` instead of overwriting the
359
+ * first request's handles. `Execute` also holds the session's lock (`acquireLock`) while it runs, so an overlapping
360
+ * request is turned away before it has any side effects, and keeps its last response (`storeResponse`) so a retry of
361
+ * the same request is answered again instead of being run twice.
90
362
  */
91
363
  export class MapiSessionManager {
92
- async init() {
93
- this.cache = await this._objectFactory.newInstance(RedisCache, {
94
- name: MapiSessionContext.name,
95
- args: [MapiSessionContext],
96
- });
364
+ init() {
365
+ this.store = this.redisClient ? new RedisMapiSessionStore(this.redisClient) : new MemoryMapiSessionStore();
366
+ this.handleDataStore = this.redisClient ? new RedisHandleDataStore(this.redisClient) : new MemoryHandleDataStore();
97
367
  }
368
+ /** Creates a session for `userUid`, ending that user's least recently used sessions (and their stored handle data)
369
+ * if they already hold `MAX_SESSIONS_PER_USER`. */
98
370
  async create(mailboxUid, userUid) {
99
371
  const context = new MapiSessionContext({ mailboxUid, userUid });
100
- await this.cache.save(context.uid, context, SESSION_TTL_SECONDS);
372
+ await this.store.create(sessionKey(context.uid), JSON.stringify(context), SESSION_TTL_SECONDS);
373
+ const ended = await this.store.addToUserIndex(userIndexKey(userUid), encodeURIComponent(context.uid), Date.now(), MAX_SESSIONS_PER_USER, MAX_SESSION_LIFETIME_MS / 1000);
374
+ for (const id of ended) {
375
+ await this.deleteHandleData(decodeURIComponent(id));
376
+ }
101
377
  return context;
102
378
  }
379
+ /** Records that `session` was just used, so the per-user cap ends the least recently used session rather than the
380
+ * oldest one. */
381
+ async touch(session) {
382
+ await this.store.touchUserIndex(userIndexKey(session.userUid), encodeURIComponent(session.uid), Date.now());
383
+ }
384
+ /** Loads a session straight from the store. A session past `MAX_SESSION_LIFETIME_MS` is ended and treated as
385
+ * missing. */
103
386
  async load(sessionId) {
104
- return this.cache.load(sessionId);
387
+ const json = await this.store.get(sessionKey(sessionId));
388
+ if (!json) {
389
+ return undefined;
390
+ }
391
+ const session = JSON.parse(json);
392
+ if (Date.now() - Date.parse(session.createdAt) > MAX_SESSION_LIFETIME_MS) {
393
+ await this.destroy(sessionId);
394
+ return undefined;
395
+ }
396
+ return session;
105
397
  }
398
+ /** Saves `context` only if nobody else saved it since it was loaded and it is within `MAX_SESSION_BYTES`,
399
+ * bumping its `version` on success. */
106
400
  async save(context) {
107
- await this.cache.save(context.uid, context, SESSION_TTL_SECONDS);
401
+ const expected = context.version;
402
+ context.version = expected + 1;
403
+ const json = JSON.stringify(context);
404
+ const result = Buffer.byteLength(json) > MAX_SESSION_BYTES
405
+ ? "tooBig"
406
+ : await this.store.compareAndSet(sessionKey(context.uid), expected, json, SESSION_TTL_SECONDS);
407
+ if (result !== "saved") {
408
+ context.version = expected;
409
+ }
410
+ return result;
108
411
  }
412
+ /** Ends a session, deleting its stored handle data too. */
109
413
  async destroy(sessionId) {
110
- await this.cache.delete(sessionId);
414
+ await this.store.delete(sessionKey(sessionId));
415
+ await this.deleteHandleData(sessionId);
416
+ }
417
+ /** Deletes every FastTransfer stream and write-stream chunk the session stored. Best effort: they expire anyway. */
418
+ async deleteHandleData(sessionId) {
419
+ await this.handleDataStore.deleteOwnedBy(sessionHandleDataIndex(sessionId)).catch(() => undefined);
420
+ }
421
+ /** Takes the session's `Execute` lock. Returns the token to release it with, or `undefined` while another
422
+ * request holds it. */
423
+ async acquireLock(sessionId) {
424
+ const token = crypto.randomUUID();
425
+ return (await this.store.acquireLock(`${sessionKey(sessionId)}.lock`, token, SESSION_LOCK_TTL_MS)) ? token : undefined;
426
+ }
427
+ async releaseLock(sessionId, token) {
428
+ await this.store.releaseLock(`${sessionKey(sessionId)}.lock`, token);
429
+ }
430
+ /** Extends the session's lock by another `SESSION_LOCK_TTL_MS`. `false` when `token` no longer holds it. */
431
+ async renewLock(sessionId, token) {
432
+ return this.store.renewLock(`${sessionKey(sessionId)}.lock`, token, SESSION_LOCK_TTL_MS);
433
+ }
434
+ /** The response last sent on this session if it answered `requestId`; `"inProgress"` while a request with that id
435
+ * is still running (or ran on a pod that died less than `SESSION_LOCK_TTL_MS` ago); otherwise `undefined`. */
436
+ async storedResponse(sessionId, requestId) {
437
+ const json = await this.store.getValue(`${sessionKey(sessionId)}.last`);
438
+ const stored = json ? JSON.parse(json) : undefined;
439
+ if (stored?.requestId !== requestId) {
440
+ return undefined;
441
+ }
442
+ return stored.body === undefined ? "inProgress" : Buffer.from(stored.body, "base64");
443
+ }
444
+ /** Records that `requestId` is running on this session, for `SESSION_LOCK_TTL_MS` (renewed along with the lock). A
445
+ * retry of it is then answered as busy instead of running a second time. */
446
+ async markInProgress(sessionId, requestId) {
447
+ await this.store.putValue(`${sessionKey(sessionId)}.last`, inProgressMarker(requestId), SESSION_LOCK_TTL_MS / 1000);
448
+ }
449
+ /** Extends the in-progress marker of `requestId` by another `SESSION_LOCK_TTL_MS`, only while it is still that marker:
450
+ * once the request's response (or anything else) has replaced it, nothing is written. `false` when it wasn't there. */
451
+ async renewInProgress(sessionId, requestId) {
452
+ // The lock scripts are exactly "extend/delete only while the value is still this one".
453
+ return this.store.renewLock(`${sessionKey(sessionId)}.last`, inProgressMarker(requestId), SESSION_LOCK_TTL_MS);
454
+ }
455
+ /** Drops the in-progress marker of `requestId` when it is still there (the request ended without storing a
456
+ * response), so a retry runs. Atomic, so it never deletes a response stored meanwhile. */
457
+ async clearInProgress(sessionId, requestId) {
458
+ await this.store.releaseLock(`${sessionKey(sessionId)}.last`, inProgressMarker(requestId));
459
+ }
460
+ /** Remembers `body` as this session's answer to `requestId`. Only the latest request is kept. */
461
+ async storeResponse(sessionId, requestId, body) {
462
+ await this.store.putValue(`${sessionKey(sessionId)}.last`, JSON.stringify({ requestId, body: body.toString("base64") }), SESSION_TTL_SECONDS);
111
463
  }
112
464
  }
465
+ __decorate([
466
+ Redis("cache", false),
467
+ __metadata("design:type", Object)
468
+ ], MapiSessionManager.prototype, "redisClient", void 0);
113
469
  __decorate([
114
470
  Init,
115
471
  __metadata("design:type", Function),
116
472
  __metadata("design:paramtypes", []),
117
- __metadata("design:returntype", Promise)
473
+ __metadata("design:returntype", void 0)
118
474
  ], MapiSessionManager.prototype, "init", null);
119
475
  //# sourceMappingURL=MapiSessionManager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MapiSessionManager.js","sourceRoot":"","sources":["../../src/MapiSessionManager.ts"],"names":[],"mappings":";;;;;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAiB,UAAU,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAClF,MAAM,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC;AAElC;8GAC8G;AAC9G,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,CAAC;AAgDpC;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,kBAAmB,SAAQ,YAAY;IA8ChD;;6GAEyG;IACzG,YAAmB,KAAsE;QACrF,KAAK,CAAC,KAAK,CAAC,CAAC;QA/CV,YAAO,GAAqC,EAAE,CAAC;QAC/C,oBAAe,GAAG,CAAC,CAAC;QACpB,cAAS,GAAW,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACpD;;+DAEuD;QAChD,cAAS,GAA2B,EAAE,CAAC;QAE9C;;;;;8GAKsG;QAC/F,oBAAe,GAA2B,EAAE,CAAC;QAC7C,iBAAY,GAAG,CAAC,CAAC;QAExB;;;qFAG6E;QACtE,eAAU,GAA2B,EAAE,CAAC;QAE/C;2FACmF;QAC5E,qBAAgB,GAA2B,EAAE,CAAC;QAC9C,kBAAa,GAAG,CAAC,CAAC;QAEzB;;;;;gHAKwG;QACjG,oBAAe,GAA2B,EAAE,CAAC;QAEpD;;;oCAG4B;QACrB,qBAAgB,GAA2B,EAAE,CAAC;QAC9C,wBAAmB,GAAG,MAAM,CAAC;QAOhC,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IACjC,CAAC;CACJ;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,kBAAkB;IAOd,AAAN,KAAK,CAAC,IAAI;QACb,IAAI,CAAC,KAAK,GAAG,MAAM,IAAI,CAAC,cAAe,CAAC,WAAW,CAAC,UAAU,EAAE;YAC5D,IAAI,EAAE,kBAAkB,CAAC,IAAI;YAC7B,IAAI,EAAE,CAAC,kBAAkB,CAAC;SAC7B,CAAC,CAAC;IACP,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,UAAkB,EAAE,OAAe;QACnD,MAAM,OAAO,GAAG,IAAI,kBAAkB,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;QAChE,MAAM,IAAI,CAAC,KAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;QAClE,OAAO,OAAO,CAAC;IACnB,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,SAAiB;QAC/B,OAAO,IAAI,CAAC,KAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvC,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,OAA2B;QACzC,MAAM,IAAI,CAAC,KAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACtE,CAAC;IAEM,KAAK,CAAC,OAAO,CAAC,SAAiB;QAClC,MAAM,IAAI,CAAC,KAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC;CACJ;AAxBgB;IADZ,IAAI;;;;8CAMJ"}
1
+ {"version":3,"file":"MapiSessionManager.js","sourceRoot":"","sources":["../../src/MapiSessionManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { StringUtils } from "@rapidrest/core";
6
+ /**
7
+ * Mirrors `@rapidrest/service-core`'s private `ModelUtils.MAX_PATTERN_LENGTH`: a `regex(...)` query operand
8
+ * longer than this is rejected as a potential ReDoS pattern with `INVALID_REQUEST`. The check runs against the
9
+ * operand AFTER escaping, so a raw client term well under the limit can still exceed it once each regex
10
+ * metacharacter doubles in length.
11
+ */
12
+ export const MAX_REGEX_PATTERN_LENGTH = 100;
13
+ /**
14
+ * Escapes `value` for a literal-substring `regex(...)` match, truncating the raw term (never mid-escape or
15
+ * mid-code-point) so the escaped result always fits `MAX_REGEX_PATTERN_LENGTH`. Matching on a prefix of an
16
+ * over-long search term still yields a superset of the full term's matches, which is a far better outcome than
17
+ * the whole command failing.
18
+ */
19
+ export function boundedEscapedPattern(value, maxLength = MAX_REGEX_PATTERN_LENGTH) {
20
+ let pattern = "";
21
+ for (const char of value) {
22
+ const escaped = StringUtils.escapeRegExp(char);
23
+ if (pattern.length + escaped.length > maxLength) {
24
+ break;
25
+ }
26
+ pattern += escaped;
27
+ }
28
+ return pattern;
29
+ }
30
+ //# sourceMappingURL=RegexPatternUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RegexPatternUtils.js","sourceRoot":"","sources":["../../src/RegexPatternUtils.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAa,EAAE,YAAoB,wBAAwB;IAC7F,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;YAC9C,MAAM;QACV,CAAC;QACD,OAAO,IAAI,OAAO,CAAC;IACvB,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC"}