agl 1.0.17 → 21.0.2

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 (277) hide show
  1. package/.smartconfig.json +55 -0
  2. package/changelog.md +1026 -0
  3. package/cli.js +14 -0
  4. package/dist_serve/bundle.js +44778 -0
  5. package/dist_serve/index.html +13 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/classes.authmodels.d.ts +126 -0
  9. package/dist_ts/classes.authmodels.js +1422 -0
  10. package/dist_ts/classes.authstore.d.ts +160 -0
  11. package/dist_ts/classes.authstore.js +2483 -0
  12. package/dist_ts/classes.browserresourcehost.d.ts +85 -0
  13. package/dist_ts/classes.browserresourcehost.js +912 -0
  14. package/dist_ts/classes.cli.d.ts +21 -0
  15. package/dist_ts/classes.cli.js +1082 -0
  16. package/dist_ts/classes.config.d.ts +23 -0
  17. package/dist_ts/classes.config.js +267 -0
  18. package/dist_ts/classes.controller.d.ts +523 -0
  19. package/dist_ts/classes.controller.js +10611 -0
  20. package/dist_ts/classes.controllermanagement.d.ts +32 -0
  21. package/dist_ts/classes.controllermanagement.js +222 -0
  22. package/dist_ts/classes.draftmanager.d.ts +30 -0
  23. package/dist_ts/classes.draftmanager.js +179 -0
  24. package/dist_ts/classes.embeddeddb.d.ts +30 -0
  25. package/dist_ts/classes.embeddeddb.js +159 -0
  26. package/dist_ts/classes.flexframedtransport.d.ts +56 -0
  27. package/dist_ts/classes.flexframedtransport.js +275 -0
  28. package/dist_ts/classes.flexmodels.private.d.ts +231 -0
  29. package/dist_ts/classes.flexmodels.private.js +985 -0
  30. package/dist_ts/classes.flexmodels.public.d.ts +207 -0
  31. package/dist_ts/classes.flexmodels.public.js +885 -0
  32. package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
  33. package/dist_ts/classes.flexprojectionreader.js +580 -0
  34. package/dist_ts/classes.flexservice.d.ts +196 -0
  35. package/dist_ts/classes.flexservice.js +3704 -0
  36. package/dist_ts/classes.flexstore.d.ts +174 -0
  37. package/dist_ts/classes.flexstore.js +3400 -0
  38. package/dist_ts/classes.flexsupervisor.d.ts +105 -0
  39. package/dist_ts/classes.flexsupervisor.js +1191 -0
  40. package/dist_ts/classes.gitreversion.d.ts +431 -0
  41. package/dist_ts/classes.gitreversion.js +10592 -0
  42. package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
  43. package/dist_ts/classes.harnessoperationmanager.js +602 -0
  44. package/dist_ts/classes.opencodeclient.d.ts +146 -0
  45. package/dist_ts/classes.opencodeclient.js +3003 -0
  46. package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
  47. package/dist_ts/classes.opencodesupervisor.js +1003 -0
  48. package/dist_ts/classes.passkeymanager.d.ts +34 -0
  49. package/dist_ts/classes.passkeymanager.js +456 -0
  50. package/dist_ts/classes.processinspection.d.ts +50 -0
  51. package/dist_ts/classes.processinspection.js +484 -0
  52. package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
  53. package/dist_ts/classes.resourcecoordinator.js +294 -0
  54. package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
  55. package/dist_ts/classes.sessionintelligencereader.js +40 -0
  56. package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
  57. package/dist_ts/classes.systemmetricssampler.js +168 -0
  58. package/dist_ts/classes.terminalmanager.d.ts +95 -0
  59. package/dist_ts/classes.terminalmanager.js +981 -0
  60. package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
  61. package/dist_ts/classes.terminalresourcehost.js +84 -0
  62. package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
  63. package/dist_ts/classes.upgradecoordinator.js +2155 -0
  64. package/dist_ts/classes.upgradetransaction.d.ts +92 -0
  65. package/dist_ts/classes.upgradetransaction.js +2125 -0
  66. package/dist_ts/classes.uploadmanager.d.ts +24 -0
  67. package/dist_ts/classes.uploadmanager.js +210 -0
  68. package/dist_ts/classes.validation.d.ts +91 -0
  69. package/dist_ts/classes.validation.js +1087 -0
  70. package/dist_ts/flexharness.child.d.ts +1 -0
  71. package/dist_ts/flexharness.child.js +28 -0
  72. package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
  73. package/dist_ts/functions.canonicaldirectory.js +144 -0
  74. package/dist_ts/functions.controllerdataroot.d.ts +15 -0
  75. package/dist_ts/functions.controllerdataroot.js +76 -0
  76. package/dist_ts/functions.embeddeddb.d.ts +2 -0
  77. package/dist_ts/functions.embeddeddb.js +21 -0
  78. package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
  79. package/dist_ts/functions.flexcredentialstore.js +93 -0
  80. package/dist_ts/functions.opencodeauth.d.ts +3 -0
  81. package/dist_ts/functions.opencodeauth.js +31 -0
  82. package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
  83. package/dist_ts/functions.runtimeenvironment.js +79 -0
  84. package/dist_ts/functions.upgradefollower.d.ts +3 -0
  85. package/dist_ts/functions.upgradefollower.js +95 -0
  86. package/dist_ts/index.d.ts +24 -0
  87. package/dist_ts/index.js +25 -0
  88. package/dist_ts/interfaces.auth.d.ts +181 -0
  89. package/dist_ts/interfaces.auth.js +82 -0
  90. package/dist_ts/interfaces.config.d.ts +34 -0
  91. package/dist_ts/interfaces.config.js +2 -0
  92. package/dist_ts/interfaces.flexipc.d.ts +939 -0
  93. package/dist_ts/interfaces.flexipc.js +1492 -0
  94. package/dist_ts/interfaces.gitreversion.d.ts +246 -0
  95. package/dist_ts/interfaces.gitreversion.js +2 -0
  96. package/dist_ts/interfaces.lifecycle.d.ts +37 -0
  97. package/dist_ts/interfaces.lifecycle.js +2 -0
  98. package/dist_ts/interfaces.opencode.d.ts +101 -0
  99. package/dist_ts/interfaces.opencode.js +3 -0
  100. package/dist_ts/interfaces.projects.d.ts +156 -0
  101. package/dist_ts/interfaces.projects.js +5 -0
  102. package/dist_ts/plugins.d.ts +36 -0
  103. package/dist_ts/plugins.js +40 -0
  104. package/dist_ts_interfaces/index.d.ts +2 -0
  105. package/dist_ts_interfaces/index.js +3 -0
  106. package/dist_ts_interfaces/interfaces.d.ts +747 -0
  107. package/dist_ts_interfaces/interfaces.js +47 -0
  108. package/dist_ts_interfaces/plugins.d.ts +4 -0
  109. package/dist_ts_interfaces/plugins.js +3 -0
  110. package/dist_ts_interfaces/requests.d.ts +806 -0
  111. package/dist_ts_interfaces/requests.js +2 -0
  112. package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
  113. package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
  114. package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
  115. package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
  116. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
  117. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
  118. package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
  119. package/dist_ts_migration/classes.migrationrunner.js +34 -0
  120. package/dist_ts_migration/index.d.ts +12 -0
  121. package/dist_ts_migration/index.js +13 -0
  122. package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
  123. package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
  124. package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
  125. package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
  126. package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
  127. package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
  128. package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
  129. package/dist_ts_migration/v18_resourcelayout.js +23 -0
  130. package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
  131. package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
  132. package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
  133. package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
  134. package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
  135. package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
  136. package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
  137. package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
  138. package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
  139. package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
  140. package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
  141. package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
  142. package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
  143. package/dist_ts_migration/v6_protocoldocuments.js +138 -0
  144. package/html/index.html +13 -0
  145. package/{LICENSE → license.md} +2 -2
  146. package/package.json +86 -48
  147. package/readme.md +480 -0
  148. package/readme.plan.md +188 -0
  149. package/ts/00_commitinfo_data.ts +8 -0
  150. package/ts/classes.authmodels.ts +1245 -0
  151. package/ts/classes.authstore.ts +3274 -0
  152. package/ts/classes.browserresourcehost.ts +1205 -0
  153. package/ts/classes.cli.ts +1254 -0
  154. package/ts/classes.config.ts +351 -0
  155. package/ts/classes.controller.ts +14426 -0
  156. package/ts/classes.controllermanagement.ts +330 -0
  157. package/ts/classes.draftmanager.ts +232 -0
  158. package/ts/classes.embeddeddb.ts +166 -0
  159. package/ts/classes.flexframedtransport.ts +283 -0
  160. package/ts/classes.flexmodels.private.ts +823 -0
  161. package/ts/classes.flexmodels.public.ts +732 -0
  162. package/ts/classes.flexprojectionreader.ts +816 -0
  163. package/ts/classes.flexservice.ts +4302 -0
  164. package/ts/classes.flexstore.ts +4565 -0
  165. package/ts/classes.flexsupervisor.ts +1453 -0
  166. package/ts/classes.gitreversion.ts +13353 -0
  167. package/ts/classes.harnessoperationmanager.ts +870 -0
  168. package/ts/classes.opencodeclient.ts +4110 -0
  169. package/ts/classes.opencodesupervisor.ts +1280 -0
  170. package/ts/classes.passkeymanager.ts +570 -0
  171. package/ts/classes.processinspection.ts +623 -0
  172. package/ts/classes.resourcecoordinator.ts +436 -0
  173. package/ts/classes.sessionintelligencereader.ts +52 -0
  174. package/ts/classes.systemmetricssampler.ts +215 -0
  175. package/ts/classes.terminalmanager.ts +1207 -0
  176. package/ts/classes.terminalresourcehost.ts +122 -0
  177. package/ts/classes.upgradecoordinator.ts +2675 -0
  178. package/ts/classes.upgradetransaction.ts +2900 -0
  179. package/ts/classes.uploadmanager.ts +240 -0
  180. package/ts/classes.validation.ts +1409 -0
  181. package/ts/flexharness.child.ts +35 -0
  182. package/ts/functions.canonicaldirectory.ts +213 -0
  183. package/ts/functions.controllerdataroot.ts +107 -0
  184. package/ts/functions.embeddeddb.ts +22 -0
  185. package/ts/functions.flexcredentialstore.ts +128 -0
  186. package/ts/functions.opencodeauth.ts +34 -0
  187. package/ts/functions.runtimeenvironment.ts +90 -0
  188. package/ts/functions.upgradefollower.ts +115 -0
  189. package/ts/index.ts +36 -6
  190. package/ts/interfaces.auth.ts +316 -0
  191. package/ts/interfaces.config.ts +37 -0
  192. package/ts/interfaces.flexipc.ts +2586 -0
  193. package/ts/interfaces.gitreversion.ts +336 -0
  194. package/ts/interfaces.lifecycle.ts +46 -0
  195. package/ts/interfaces.opencode.ts +153 -0
  196. package/ts/interfaces.projects.ts +182 -0
  197. package/ts/plugins.ts +64 -0
  198. package/ts_interfaces/index.ts +2 -0
  199. package/ts_interfaces/interfaces.ts +827 -0
  200. package/ts_interfaces/plugins.ts +15 -0
  201. package/ts_interfaces/requests.ts +1186 -0
  202. package/ts_migration/classes.documentmigrationrunner.ts +32 -0
  203. package/ts_migration/classes.flexmigrationrunner.ts +33 -0
  204. package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
  205. package/ts_migration/classes.migrationrunner.ts +40 -0
  206. package/ts_migration/index.ts +12 -0
  207. package/ts_migration/v10_flexharnessv3migration.ts +35 -0
  208. package/ts_migration/v13_flexprojectionorder.ts +33 -0
  209. package/ts_migration/v16_flexprovidercredentials.ts +196 -0
  210. package/ts_migration/v18_resourcelayout.ts +55 -0
  211. package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
  212. package/ts_migration/v19_flexprojectmanagement.ts +28 -0
  213. package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
  214. package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
  215. package/ts_migration/v2_controllerdataroot.ts +2349 -0
  216. package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
  217. package/ts_migration/v6_protocoldocuments.ts +198 -0
  218. package/ts_web/00_commitinfo_data.ts +8 -0
  219. package/ts_web/classes.controllersocketclient.ts +517 -0
  220. package/ts_web/classes.sessiondraftsync.ts +390 -0
  221. package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
  222. package/ts_web/index.ts +2 -0
  223. package/ts_web/plugins.ts +31 -0
  224. package/.gitlab-ci.yml +0 -71
  225. package/.npmignore +0 -5
  226. package/README.md +0 -40
  227. package/dist/cli.js +0 -2
  228. package/dist/index.d.ts +0 -0
  229. package/dist/index.js +0 -9
  230. package/dist/mod_bundle/index.d.ts +0 -1
  231. package/dist/mod_bundle/index.js +0 -18
  232. package/dist/mod_bundle/mod.plugins.d.ts +0 -4
  233. package/dist/mod_bundle/mod.plugins.js +0 -11
  234. package/dist/mod_install/index.d.ts +0 -4
  235. package/dist/mod_install/index.js +0 -21
  236. package/dist/mod_install/mod.plugins.d.ts +0 -1
  237. package/dist/mod_install/mod.plugins.js +0 -7
  238. package/dist/mod_standard/index.d.ts +0 -4
  239. package/dist/mod_standard/index.js +0 -21
  240. package/dist/mod_standard/mod.browser.d.ts +0 -6
  241. package/dist/mod_standard/mod.browser.js +0 -14
  242. package/dist/mod_standard/mod.clean.d.ts +0 -4
  243. package/dist/mod_standard/mod.clean.js +0 -12
  244. package/dist/mod_standard/mod.compile.d.ts +0 -4
  245. package/dist/mod_standard/mod.compile.js +0 -21
  246. package/dist/mod_standard/mod.plugins.d.ts +0 -8
  247. package/dist/mod_standard/mod.plugins.js +0 -19
  248. package/dist/mod_standard/mod.test.d.ts +0 -4
  249. package/dist/mod_standard/mod.test.js +0 -12
  250. package/dist/plr.cli.d.ts +0 -3
  251. package/dist/plr.cli.js +0 -57
  252. package/dist/plr.config.d.ts +0 -11
  253. package/dist/plr.config.js +0 -26
  254. package/dist/plr.mods.d.ts +0 -17
  255. package/dist/plr.mods.js +0 -17
  256. package/dist/plr.paths.d.ts +0 -19
  257. package/dist/plr.paths.js +0 -26
  258. package/dist/plr.plugins.d.ts +0 -8
  259. package/dist/plr.plugins.js +0 -16
  260. package/npmextra.json +0 -10
  261. package/ts/mod_bundle/index.ts +0 -20
  262. package/ts/mod_bundle/mod.plugins.ts +0 -9
  263. package/ts/mod_install/index.ts +0 -20
  264. package/ts/mod_install/mod.plugins.ts +0 -1
  265. package/ts/mod_standard/index.ts +0 -23
  266. package/ts/mod_standard/mod.browser.ts +0 -17
  267. package/ts/mod_standard/mod.clean.ts +0 -13
  268. package/ts/mod_standard/mod.compile.ts +0 -23
  269. package/ts/mod_standard/mod.plugins.ts +0 -17
  270. package/ts/mod_standard/mod.test.ts +0 -13
  271. package/ts/plr.cli.ts +0 -60
  272. package/ts/plr.config.ts +0 -36
  273. package/ts/plr.mods.ts +0 -25
  274. package/ts/plr.paths.ts +0 -29
  275. package/ts/plr.plugins.ts +0 -17
  276. package/tslint.json +0 -3
  277. package/yarn.lock +0 -3464
@@ -0,0 +1,816 @@
1
+ import * as plugins from './plugins.js';
2
+ import type {
3
+ IFlexDatabaseDescriptor,
4
+ IFlexPublicProviderConnection,
5
+ } from './interfaces.flexipc.js';
6
+ import { hasFlexExactKeys, isFlexPlainObject } from './interfaces.flexipc.js';
7
+ import {
8
+ assertFlexProviderConnectionDocument,
9
+ type IFlexProviderConnectionDocument,
10
+ } from './classes.flexmodels.private.js';
11
+ import {
12
+ FlexPublicCandidateModel,
13
+ FlexPublicMessageRecordModel,
14
+ FlexPublicSessionRecordModel,
15
+ type IFlexPublicCandidateDocument,
16
+ type IFlexPublicCandidateManifest,
17
+ type IFlexPublicMessageRecordDocument,
18
+ type IFlexPublicSessionRecordDocument,
19
+ type IFlexRecordManifest,
20
+ assertFlexPublicCandidateDocument,
21
+ assertFlexPublicMessageRecordDocument,
22
+ assertFlexPublicSessionRecordDocument,
23
+ flexPublicHeadId,
24
+ flexPublicPageBytesLimit,
25
+ flexPublicPageLimit,
26
+ initFlexPublicModels,
27
+ isFlexPublicCandidateManifest,
28
+ registerFlexPublicModels,
29
+ } from './classes.flexmodels.public.js';
30
+
31
+ type TFlexMessage = plugins.flexharness.IFlexMessage;
32
+ type TFlexSession = plugins.flexharness.IFlexSession;
33
+
34
+ export interface IFlexProjectionReaderOptions {
35
+ database: IFlexDatabaseDescriptor;
36
+ controllerId: string;
37
+ }
38
+
39
+ export interface IFlexProjectionPageOptions {
40
+ limit?: number;
41
+ cursor?: string;
42
+ }
43
+
44
+ export interface IFlexProjectedSessionPage {
45
+ candidateId: string;
46
+ revision: number;
47
+ sessions: TFlexSession[];
48
+ nextCursor?: string;
49
+ truncated: boolean;
50
+ }
51
+
52
+ export interface IFlexProjectedMessagePage {
53
+ candidateId: string;
54
+ revision: number;
55
+ messages: IFlexProjectedMessage[];
56
+ nextCursor?: string;
57
+ truncated: boolean;
58
+ }
59
+
60
+ export interface IFlexProjectedMessage {
61
+ messageIndex: number;
62
+ message: TFlexMessage;
63
+ }
64
+
65
+ export interface IFlexProjectedSessionSnapshot {
66
+ candidateId: string;
67
+ revision: number;
68
+ sessions: TFlexSession[];
69
+ truncated: boolean;
70
+ }
71
+
72
+ export interface IFlexProjectedMessageSnapshot {
73
+ candidateId: string;
74
+ revision: number;
75
+ messages: IFlexProjectedMessage[];
76
+ truncated: boolean;
77
+ }
78
+
79
+ interface IFlexPublicHeadView {
80
+ id: string;
81
+ controllerId: string;
82
+ storageKey: string;
83
+ revision: number;
84
+ currentPublicCandidateId: string;
85
+ publicCandidates: IFlexPublicCandidateManifest[];
86
+ }
87
+
88
+ interface IFlexProjectionCursor {
89
+ version: 2;
90
+ kind: 'sessions' | 'messages';
91
+ controllerId: string;
92
+ scopeId: string;
93
+ candidateId: string;
94
+ revision: number;
95
+ anchorId: string;
96
+ anchorTimestamp?: string;
97
+ anchorMessageIndex?: number;
98
+ sessionId?: string;
99
+ }
100
+
101
+ interface IFlexCandidateSelection {
102
+ head: IFlexPublicHeadView;
103
+ manifest: IFlexPublicCandidateManifest;
104
+ candidate: IFlexPublicCandidateDocument;
105
+ }
106
+
107
+ const sha256 = (valueArg: Uint8Array | string): string =>
108
+ plugins.crypto.createHash('sha256').update(valueArg).digest('hex');
109
+
110
+ const serializedBytes = (valueArg: unknown): Buffer => Buffer.from(JSON.stringify(valueArg), 'utf8');
111
+
112
+ const isNonNegativeInteger = (valueArg: unknown): valueArg is number =>
113
+ Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
114
+
115
+ const isBoundedString = (valueArg: unknown, maximumBytesArg: number): valueArg is string =>
116
+ typeof valueArg === 'string'
117
+ && valueArg.length > 0
118
+ && Buffer.byteLength(valueArg, 'utf8') <= maximumBytesArg;
119
+
120
+ const isIsoDate = (valueArg: unknown): valueArg is string =>
121
+ typeof valueArg === 'string'
122
+ && Number.isFinite(Date.parse(valueArg))
123
+ && new Date(valueArg).toISOString() === valueArg;
124
+
125
+ function assertPublicHeadView(valueArg: unknown): asserts valueArg is IFlexPublicHeadView {
126
+ if (
127
+ !isFlexPlainObject(valueArg)
128
+ || !hasFlexExactKeys(valueArg, [
129
+ 'id', 'controllerId', 'storageKey', 'revision',
130
+ 'currentPublicCandidateId', 'publicCandidates',
131
+ ])
132
+ || !isBoundedString(valueArg.id, 1024)
133
+ || !isBoundedString(valueArg.controllerId, 512)
134
+ || !isBoundedString(valueArg.storageKey, 512)
135
+ || !isNonNegativeInteger(valueArg.revision)
136
+ || !isBoundedString(valueArg.currentPublicCandidateId, 64)
137
+ || !Array.isArray(valueArg.publicCandidates)
138
+ || valueArg.publicCandidates.length < 1
139
+ || valueArg.publicCandidates.length > 4
140
+ || !valueArg.publicCandidates.every(isFlexPublicCandidateManifest)
141
+ || valueArg.publicCandidates[0]?.candidateId !== valueArg.currentPublicCandidateId
142
+ || valueArg.publicCandidates[0]?.revision !== valueArg.revision
143
+ ) throw new FlexProjectionFormatError();
144
+ }
145
+
146
+ const projectCandidate = (modelArg: FlexPublicCandidateModel): IFlexPublicCandidateDocument => ({
147
+ id: modelArg.id,
148
+ controllerId: modelArg.controllerId,
149
+ scopeId: modelArg.scopeId,
150
+ candidateId: modelArg.candidateId,
151
+ revision: modelArg.revision,
152
+ sha256: modelArg.sha256,
153
+ bytes: modelArg.bytes,
154
+ sessionRecords: modelArg.sessionRecords,
155
+ messageRecords: modelArg.messageRecords,
156
+ sessionsTruncated: modelArg.sessionsTruncated,
157
+ messagesTruncated: modelArg.messagesTruncated,
158
+ messageBytesTruncated: modelArg.messageBytesTruncated,
159
+ visibleDigest: modelArg.visibleDigest,
160
+ scopeSource: modelArg.scopeSource,
161
+ projectionSources: modelArg.projectionSources,
162
+ createdAt: modelArg.createdAt,
163
+ });
164
+
165
+ const projectSessionRecord = (
166
+ modelArg: FlexPublicSessionRecordModel,
167
+ ): IFlexPublicSessionRecordDocument => ({
168
+ id: modelArg.id,
169
+ controllerId: modelArg.controllerId,
170
+ scopeId: modelArg.scopeId,
171
+ candidateId: modelArg.candidateId,
172
+ revision: modelArg.revision,
173
+ session: modelArg.session,
174
+ createdAt: modelArg.createdAt,
175
+ });
176
+
177
+ const projectMessageRecord = (
178
+ modelArg: FlexPublicMessageRecordModel,
179
+ ): IFlexPublicMessageRecordDocument => ({
180
+ id: modelArg.id,
181
+ controllerId: modelArg.controllerId,
182
+ scopeId: modelArg.scopeId,
183
+ candidateId: modelArg.candidateId,
184
+ revision: modelArg.revision,
185
+ messageIndex: modelArg.messageIndex,
186
+ message: modelArg.message,
187
+ createdAt: modelArg.createdAt,
188
+ });
189
+
190
+ const compareNewestSession = (
191
+ leftArg: IFlexPublicSessionRecordDocument,
192
+ rightArg: IFlexPublicSessionRecordDocument,
193
+ ): number => rightArg.session.updatedAt.localeCompare(leftArg.session.updatedAt)
194
+ || rightArg.session.createdAt.localeCompare(leftArg.session.createdAt)
195
+ || leftArg.session.sessionId.localeCompare(rightArg.session.sessionId);
196
+
197
+ const compareNewestMessage = (
198
+ leftArg: IFlexPublicMessageRecordDocument,
199
+ rightArg: IFlexPublicMessageRecordDocument,
200
+ ): number => rightArg.messageIndex - leftArg.messageIndex;
201
+
202
+ const validateLimit = (valueArg: number | undefined): number => {
203
+ const selected = valueArg ?? flexPublicPageLimit;
204
+ if (!Number.isSafeInteger(selected) || selected < 1 || selected > flexPublicPageLimit) {
205
+ throw new FlexProjectionCursorError();
206
+ }
207
+ return selected;
208
+ };
209
+
210
+ const manifestBytes = (manifestArg: IFlexPublicCandidateManifest): Buffer => serializedBytes({
211
+ candidateId: manifestArg.candidateId,
212
+ revision: manifestArg.revision,
213
+ sessionRecords: manifestArg.sessionRecords.map(({ id, sha256: digest, bytes }) => ({
214
+ id, sha256: digest, bytes,
215
+ })),
216
+ messageRecords: manifestArg.messageRecords.map(({ id, sha256: digest, bytes }) => ({
217
+ id, sha256: digest, bytes,
218
+ })),
219
+ sessionsTruncated: manifestArg.sessionsTruncated,
220
+ messagesTruncated: manifestArg.messagesTruncated,
221
+ messageBytesTruncated: manifestArg.messageBytesTruncated,
222
+ visibleDigest: manifestArg.visibleDigest,
223
+ scopeSource: manifestArg.scopeSource,
224
+ projectionSources: manifestArg.projectionSources,
225
+ });
226
+
227
+ const verifyRecordManifest = (documentArg: { id: string }, manifestArg: IFlexRecordManifest): void => {
228
+ const bytes = serializedBytes(documentArg);
229
+ if (
230
+ documentArg.id !== manifestArg.id
231
+ || bytes.byteLength !== manifestArg.bytes
232
+ || sha256(bytes) !== manifestArg.sha256
233
+ ) throw new FlexProjectionFormatError();
234
+ };
235
+
236
+ const recordManifestsEqual = (
237
+ leftArg: IFlexRecordManifest[],
238
+ rightArg: IFlexRecordManifest[],
239
+ ): boolean => leftArg.length === rightArg.length && leftArg.every((left, index) => {
240
+ const right = rightArg[index];
241
+ return right !== undefined
242
+ && left.id === right.id
243
+ && left.sha256 === right.sha256
244
+ && left.bytes === right.bytes;
245
+ });
246
+
247
+ export class FlexProjectionCursorError extends Error {
248
+ public readonly code = 'FLEX_PROJECTION_CURSOR_INVALID';
249
+ constructor() {
250
+ super('The Flex projection cursor is invalid.');
251
+ }
252
+ }
253
+
254
+ export class FlexProjectionStaleCursorError extends Error {
255
+ public readonly code = 'FLEX_PROJECTION_CURSOR_STALE';
256
+ constructor() {
257
+ super('The Flex projection cursor refers to an evicted candidate.');
258
+ }
259
+ }
260
+
261
+ export class FlexProjectionNotFoundError extends Error {
262
+ public readonly code = 'FLEX_PROJECTION_NOT_FOUND';
263
+ constructor() {
264
+ super('The requested Flex projection was not found.');
265
+ }
266
+ }
267
+
268
+ export class FlexProjectionFormatError extends Error {
269
+ public readonly code = 'FLEX_PROJECTION_FORMAT_INVALID';
270
+ constructor() {
271
+ super('The Flex public projection is invalid.');
272
+ }
273
+ }
274
+
275
+ export class FlexProjectionReader {
276
+ private readonly database: plugins.smartdata.SmartdataDb;
277
+ private readonly controllerId: string;
278
+ private readonly manager: { db: plugins.smartdata.SmartdataDb };
279
+ private initialized = false;
280
+ private databaseOpened = false;
281
+ private initPromise?: Promise<void>;
282
+ private closePromise?: Promise<void>;
283
+ private closed = false;
284
+
285
+ constructor(optionsArg: IFlexProjectionReaderOptions) {
286
+ this.controllerId = optionsArg.controllerId;
287
+ this.database = new plugins.smartdata.SmartdataDb(optionsArg.database);
288
+ this.manager = { db: this.database };
289
+ registerFlexPublicModels(this.manager);
290
+ }
291
+
292
+ public async init(): Promise<void> {
293
+ if (this.closed) throw new Error('The Flex projection reader is closed.');
294
+ if (this.initialized) return;
295
+ if (this.initPromise) return this.initPromise;
296
+ const initPromise = (async () => {
297
+ try {
298
+ this.databaseOpened = true;
299
+ await this.database.init();
300
+ await initFlexPublicModels();
301
+ if (this.closed) throw new Error('The Flex projection reader was closed during startup.');
302
+ this.initialized = true;
303
+ } catch (errorArg) {
304
+ if (this.databaseOpened) {
305
+ try {
306
+ await this.database.close();
307
+ this.databaseOpened = false;
308
+ } catch {
309
+ // close() retains retry ownership for a database that did not close.
310
+ }
311
+ }
312
+ throw errorArg;
313
+ }
314
+ })();
315
+ this.initPromise = initPromise;
316
+ try {
317
+ await initPromise;
318
+ } finally {
319
+ if (this.initPromise === initPromise) this.initPromise = undefined;
320
+ }
321
+ }
322
+
323
+ public async close(): Promise<void> {
324
+ if (this.closePromise) return this.closePromise;
325
+ this.closed = true;
326
+ const closePromise = (async () => {
327
+ if (this.initPromise) await this.initPromise.catch(() => undefined);
328
+ if (this.databaseOpened) {
329
+ await this.database.close();
330
+ this.databaseOpened = false;
331
+ }
332
+ this.initialized = false;
333
+ })();
334
+ this.closePromise = closePromise;
335
+ try {
336
+ await closePromise;
337
+ } finally {
338
+ if (this.closePromise === closePromise) this.closePromise = undefined;
339
+ }
340
+ }
341
+
342
+ public async listSessionPage(
343
+ scopeIdArg: string,
344
+ optionsArg: IFlexProjectionPageOptions = {},
345
+ signalArg?: AbortSignal,
346
+ ): Promise<IFlexProjectedSessionPage> {
347
+ const limit = validateLimit(optionsArg.limit);
348
+ const cursor = optionsArg.cursor
349
+ ? this.parseCursor(optionsArg.cursor, 'sessions', scopeIdArg)
350
+ : undefined;
351
+ const selection = await this.selectCandidate(scopeIdArg, cursor, signalArg);
352
+ const records = await this.loadSessionRecords(selection, signalArg);
353
+ const startIndex = cursor
354
+ ? this.findAnchorIndex(
355
+ records,
356
+ cursor.anchorId,
357
+ cursor.anchorTimestamp!,
358
+ (record) => record.session.sessionId,
359
+ (record) => record.session.updatedAt,
360
+ ) + 1
361
+ : 0;
362
+ let selected = records.slice(startIndex, startIndex + limit);
363
+ let page = this.createSessionPage(selection, selected, startIndex + selected.length < records.length);
364
+ while (serializedBytes(page).byteLength > flexPublicPageBytesLimit && selected.length > 0) {
365
+ selected = selected.slice(0, -1);
366
+ page = this.createSessionPage(selection, selected, true);
367
+ }
368
+ if (selected.length === 0 && records.length > startIndex) throw new FlexProjectionFormatError();
369
+ return page;
370
+ }
371
+
372
+ public async listAllSessions(
373
+ scopeIdArg: string,
374
+ signalArg?: AbortSignal,
375
+ ): Promise<IFlexProjectedSessionSnapshot> {
376
+ const selection = await this.selectCandidate(scopeIdArg, undefined, signalArg);
377
+ const records = await this.loadSessionRecords(selection, signalArg);
378
+ return {
379
+ candidateId: selection.manifest.candidateId,
380
+ revision: selection.manifest.revision,
381
+ sessions: records.map((record) => JSON.parse(JSON.stringify(record.session)) as TFlexSession),
382
+ truncated: selection.manifest.sessionsTruncated,
383
+ };
384
+ }
385
+
386
+ public async getSession(
387
+ scopeIdArg: string,
388
+ sessionIdArg: string,
389
+ signalArg?: AbortSignal,
390
+ ): Promise<TFlexSession> {
391
+ const selection = await this.selectCandidate(scopeIdArg, undefined, signalArg);
392
+ const records = await this.loadSessionRecords(selection, signalArg);
393
+ const record = records.find((entry) => entry.session.sessionId === sessionIdArg);
394
+ if (!record) throw new FlexProjectionNotFoundError();
395
+ return JSON.parse(JSON.stringify(record.session)) as TFlexSession;
396
+ }
397
+
398
+ public async listMessagePage(
399
+ scopeIdArg: string,
400
+ sessionIdArg: string,
401
+ optionsArg: IFlexProjectionPageOptions = {},
402
+ signalArg?: AbortSignal,
403
+ ): Promise<IFlexProjectedMessagePage> {
404
+ const limit = validateLimit(optionsArg.limit);
405
+ const cursor = optionsArg.cursor
406
+ ? this.parseCursor(optionsArg.cursor, 'messages', scopeIdArg, sessionIdArg)
407
+ : undefined;
408
+ const selection = await this.selectCandidate(scopeIdArg, cursor, signalArg);
409
+ const records = (await this.loadMessageRecords(selection, signalArg))
410
+ .filter((entry) => entry.message.sessionId === sessionIdArg)
411
+ .sort(compareNewestMessage);
412
+ const startIndex = cursor
413
+ ? records.findIndex((record) => (
414
+ record.message.messageId === cursor.anchorId
415
+ && record.messageIndex === cursor.anchorMessageIndex
416
+ )) + 1
417
+ : 0;
418
+ if (cursor && startIndex === 0) throw new FlexProjectionCursorError();
419
+ let newestFirst = records.slice(startIndex, startIndex + limit);
420
+ let page = this.createMessagePage(
421
+ selection,
422
+ sessionIdArg,
423
+ newestFirst,
424
+ startIndex + newestFirst.length < records.length,
425
+ );
426
+ while (serializedBytes(page).byteLength > flexPublicPageBytesLimit && newestFirst.length > 0) {
427
+ newestFirst = newestFirst.slice(0, -1);
428
+ page = this.createMessagePage(selection, sessionIdArg, newestFirst, true);
429
+ }
430
+ if (newestFirst.length === 0 && records.length > startIndex) throw new FlexProjectionFormatError();
431
+ return page;
432
+ }
433
+
434
+ public async listAllSessionMessages(
435
+ scopeIdArg: string,
436
+ sessionIdArg: string,
437
+ signalArg?: AbortSignal,
438
+ ): Promise<IFlexProjectedMessageSnapshot> {
439
+ const selection = await this.selectCandidate(scopeIdArg, undefined, signalArg);
440
+ const records = (await this.loadMessageRecords(selection, signalArg))
441
+ .filter((entry) => entry.message.sessionId === sessionIdArg)
442
+ .sort(compareNewestMessage);
443
+ return {
444
+ candidateId: selection.manifest.candidateId,
445
+ revision: selection.manifest.revision,
446
+ messages: records.map((record) => ({
447
+ messageIndex: record.messageIndex,
448
+ message: JSON.parse(JSON.stringify(record.message)) as TFlexMessage,
449
+ })).reverse(),
450
+ truncated: selection.manifest.messagesTruncated
451
+ || selection.manifest.messageBytesTruncated,
452
+ };
453
+ }
454
+
455
+ public async getMessage(
456
+ scopeIdArg: string,
457
+ sessionIdArg: string,
458
+ messageIdArg: string,
459
+ signalArg?: AbortSignal,
460
+ ): Promise<IFlexProjectedMessage> {
461
+ const selection = await this.selectCandidate(scopeIdArg, undefined, signalArg);
462
+ const records = await this.loadMessageRecords(selection, signalArg);
463
+ const record = records.find((entry) => (
464
+ entry.message.sessionId === sessionIdArg && entry.message.messageId === messageIdArg
465
+ ));
466
+ if (!record) throw new FlexProjectionNotFoundError();
467
+ return {
468
+ messageIndex: record.messageIndex,
469
+ message: JSON.parse(JSON.stringify(record.message)) as TFlexMessage,
470
+ };
471
+ }
472
+
473
+ public async listProviderConnections(
474
+ signalArg?: AbortSignal,
475
+ ): Promise<IFlexPublicProviderConnection[]> {
476
+ signalArg?.throwIfAborted();
477
+ await this.init();
478
+ signalArg?.throwIfAborted();
479
+ const cursor = this.database.mongoDb.collection('flex_provider_connections').find(
480
+ { controllerId: this.controllerId },
481
+ {
482
+ projection: {
483
+ _id: 0,
484
+ id: 1,
485
+ controllerId: 1,
486
+ providerId: 1,
487
+ state: 1,
488
+ generation: 1,
489
+ createdAt: 1,
490
+ updatedAt: 1,
491
+ account: 1,
492
+ },
493
+ sort: { updatedAt: -1, id: 1 },
494
+ limit: 513,
495
+ ...(signalArg === undefined ? {} : { signal: signalArg }),
496
+ },
497
+ );
498
+ try {
499
+ const rawDocuments = await cursor.toArray() as unknown[];
500
+ if (rawDocuments.length > 512) {
501
+ throw new Error('The persisted Flex provider connection limit was exceeded.');
502
+ }
503
+ return rawDocuments.map((rawDocument) => {
504
+ assertFlexProviderConnectionDocument(rawDocument);
505
+ const document = rawDocument as IFlexProviderConnectionDocument;
506
+ if (document.controllerId !== this.controllerId) throw new FlexProjectionFormatError();
507
+ return {
508
+ loginId: document.id,
509
+ providerId: document.providerId,
510
+ status: document.state,
511
+ ...(document.account === undefined
512
+ ? {}
513
+ : { account: JSON.parse(JSON.stringify(document.account)) as typeof document.account }),
514
+ };
515
+ });
516
+ } finally {
517
+ await cursor.close();
518
+ }
519
+ }
520
+
521
+ private async selectCandidate(
522
+ scopeIdArg: string,
523
+ cursorArg?: IFlexProjectionCursor,
524
+ signalArg?: AbortSignal,
525
+ ): Promise<IFlexCandidateSelection> {
526
+ signalArg?.throwIfAborted();
527
+ await this.init();
528
+ signalArg?.throwIfAborted();
529
+ if (cursorArg) {
530
+ const head = await this.loadHead(scopeIdArg, signalArg);
531
+ const manifest = head.publicCandidates.find((candidate) => (
532
+ candidate.candidateId === cursorArg.candidateId
533
+ && candidate.revision === cursorArg.revision
534
+ ));
535
+ if (!manifest) throw new FlexProjectionStaleCursorError();
536
+ const candidate = await this.loadCandidate(scopeIdArg, manifest, signalArg);
537
+ if (!candidate) throw new FlexProjectionStaleCursorError();
538
+ return { head, manifest, candidate };
539
+ }
540
+
541
+ let head = await this.loadHead(scopeIdArg, signalArg);
542
+ let manifest = head.publicCandidates.find(
543
+ (candidate) => candidate.candidateId === head.currentPublicCandidateId,
544
+ );
545
+ if (!manifest) throw new FlexProjectionFormatError();
546
+ let candidate = await this.loadCandidate(scopeIdArg, manifest, signalArg);
547
+ if (!candidate) {
548
+ head = await this.loadHead(scopeIdArg, signalArg);
549
+ manifest = head.publicCandidates.find(
550
+ (entry) => entry.candidateId === head.currentPublicCandidateId,
551
+ );
552
+ if (!manifest) throw new FlexProjectionFormatError();
553
+ candidate = await this.loadCandidate(scopeIdArg, manifest, signalArg);
554
+ }
555
+ if (!candidate) throw new FlexProjectionFormatError();
556
+ return { head, manifest, candidate };
557
+ }
558
+
559
+ private async loadHead(
560
+ scopeIdArg: string,
561
+ signalArg?: AbortSignal,
562
+ ): Promise<IFlexPublicHeadView> {
563
+ const raw = await this.database.mongoDb.collection('flex_public_heads').findOne(
564
+ { id: flexPublicHeadId(this.controllerId, scopeIdArg) },
565
+ {
566
+ projection: {
567
+ _id: 0,
568
+ id: 1,
569
+ controllerId: 1,
570
+ storageKey: 1,
571
+ revision: 1,
572
+ currentPublicCandidateId: 1,
573
+ publicCandidates: 1,
574
+ },
575
+ ...(signalArg === undefined ? {} : { signal: signalArg }),
576
+ },
577
+ ) as unknown;
578
+ if (!raw) throw new FlexProjectionNotFoundError();
579
+ assertPublicHeadView(raw);
580
+ if (raw.controllerId !== this.controllerId || raw.storageKey !== scopeIdArg) {
581
+ throw new FlexProjectionFormatError();
582
+ }
583
+ return raw;
584
+ }
585
+
586
+ private async loadCandidate(
587
+ scopeIdArg: string,
588
+ manifestArg: IFlexPublicCandidateManifest,
589
+ signalArg?: AbortSignal,
590
+ ): Promise<IFlexPublicCandidateDocument | undefined> {
591
+ const raw = await FlexPublicCandidateModel.collection.mongoDbCollection.findOne({
592
+ candidateId: manifestArg.candidateId,
593
+ controllerId: this.controllerId,
594
+ scopeId: scopeIdArg,
595
+ }, {
596
+ projection: { _id: 0 },
597
+ ...(signalArg === undefined ? {} : { signal: signalArg }),
598
+ });
599
+ if (!raw) return undefined;
600
+ const candidate = projectCandidate(raw as unknown as FlexPublicCandidateModel);
601
+ assertFlexPublicCandidateDocument(candidate);
602
+ const bytes = manifestBytes(manifestArg);
603
+ if (
604
+ candidate.candidateId !== manifestArg.candidateId
605
+ || candidate.revision !== manifestArg.revision
606
+ || candidate.sha256 !== manifestArg.sha256
607
+ || candidate.bytes !== manifestArg.bytes
608
+ || !recordManifestsEqual(candidate.sessionRecords, manifestArg.sessionRecords)
609
+ || !recordManifestsEqual(candidate.messageRecords, manifestArg.messageRecords)
610
+ || candidate.sessionsTruncated !== manifestArg.sessionsTruncated
611
+ || candidate.messagesTruncated !== manifestArg.messagesTruncated
612
+ || candidate.messageBytesTruncated !== manifestArg.messageBytesTruncated
613
+ || bytes.byteLength !== manifestArg.bytes
614
+ || sha256(bytes) !== manifestArg.sha256
615
+ ) throw new FlexProjectionFormatError();
616
+ return candidate;
617
+ }
618
+
619
+ private async loadSessionRecords(
620
+ selectionArg: IFlexCandidateSelection,
621
+ signalArg?: AbortSignal,
622
+ ): Promise<IFlexPublicSessionRecordDocument[]> {
623
+ const cursor = FlexPublicSessionRecordModel.collection.mongoDbCollection.find({
624
+ candidateId: selectionArg.candidate.candidateId,
625
+ controllerId: this.controllerId,
626
+ scopeId: selectionArg.head.storageKey,
627
+ }, {
628
+ projection: { _id: 0 },
629
+ ...(signalArg === undefined ? {} : { signal: signalArg }),
630
+ });
631
+ try {
632
+ const documents = await cursor.toArray();
633
+ if (documents.length !== selectionArg.manifest.sessionRecords.length) {
634
+ throw new FlexProjectionFormatError();
635
+ }
636
+ const manifestById = new Map(
637
+ selectionArg.manifest.sessionRecords.map((manifest) => [manifest.id, manifest]),
638
+ );
639
+ return documents.map((raw) => {
640
+ const document = projectSessionRecord(raw as unknown as FlexPublicSessionRecordModel);
641
+ assertFlexPublicSessionRecordDocument(document);
642
+ const manifest = manifestById.get(document.id);
643
+ if (!manifest || document.revision !== selectionArg.manifest.revision) {
644
+ throw new FlexProjectionFormatError();
645
+ }
646
+ verifyRecordManifest(document, manifest);
647
+ return document;
648
+ }).sort(compareNewestSession);
649
+ } finally {
650
+ await cursor.close();
651
+ }
652
+ }
653
+
654
+ private async loadMessageRecords(
655
+ selectionArg: IFlexCandidateSelection,
656
+ signalArg?: AbortSignal,
657
+ ): Promise<IFlexPublicMessageRecordDocument[]> {
658
+ const cursor = FlexPublicMessageRecordModel.collection.mongoDbCollection.find({
659
+ candidateId: selectionArg.candidate.candidateId,
660
+ controllerId: this.controllerId,
661
+ scopeId: selectionArg.head.storageKey,
662
+ }, {
663
+ projection: { _id: 0 },
664
+ ...(signalArg === undefined ? {} : { signal: signalArg }),
665
+ });
666
+ try {
667
+ const documents = await cursor.toArray();
668
+ if (documents.length !== selectionArg.manifest.messageRecords.length) {
669
+ throw new FlexProjectionFormatError();
670
+ }
671
+ const manifestById = new Map(
672
+ selectionArg.manifest.messageRecords.map((manifest) => [manifest.id, manifest]),
673
+ );
674
+ const records = documents.map((raw) => {
675
+ const document = projectMessageRecord(raw as unknown as FlexPublicMessageRecordModel);
676
+ assertFlexPublicMessageRecordDocument(document);
677
+ const manifest = manifestById.get(document.id);
678
+ if (!manifest || document.revision !== selectionArg.manifest.revision) {
679
+ throw new FlexProjectionFormatError();
680
+ }
681
+ verifyRecordManifest(document, manifest);
682
+ return document;
683
+ });
684
+ if (new Set(records.map((record) => (
685
+ `${record.message.sessionId}\0${record.messageIndex}`
686
+ ))).size !== records.length) throw new FlexProjectionFormatError();
687
+ return records;
688
+ } finally {
689
+ await cursor.close();
690
+ }
691
+ }
692
+
693
+ private createSessionPage(
694
+ selectionArg: IFlexCandidateSelection,
695
+ recordsArg: IFlexPublicSessionRecordDocument[],
696
+ hasMoreArg: boolean,
697
+ ): IFlexProjectedSessionPage {
698
+ const anchor = recordsArg.at(-1);
699
+ return {
700
+ candidateId: selectionArg.manifest.candidateId,
701
+ revision: selectionArg.manifest.revision,
702
+ sessions: recordsArg.map((record) => JSON.parse(JSON.stringify(record.session)) as TFlexSession),
703
+ ...(hasMoreArg && anchor ? {
704
+ nextCursor: this.encodeCursor({
705
+ version: 2,
706
+ kind: 'sessions',
707
+ controllerId: this.controllerId,
708
+ scopeId: selectionArg.head.storageKey,
709
+ candidateId: selectionArg.manifest.candidateId,
710
+ revision: selectionArg.manifest.revision,
711
+ anchorId: anchor.session.sessionId,
712
+ anchorTimestamp: anchor.session.updatedAt,
713
+ }),
714
+ } : {}),
715
+ truncated: selectionArg.manifest.sessionsTruncated,
716
+ };
717
+ }
718
+
719
+ private createMessagePage(
720
+ selectionArg: IFlexCandidateSelection,
721
+ sessionIdArg: string,
722
+ newestFirstArg: IFlexPublicMessageRecordDocument[],
723
+ hasMoreArg: boolean,
724
+ ): IFlexProjectedMessagePage {
725
+ const anchor = newestFirstArg.at(-1);
726
+ return {
727
+ candidateId: selectionArg.manifest.candidateId,
728
+ revision: selectionArg.manifest.revision,
729
+ messages: newestFirstArg
730
+ .map((record) => ({
731
+ messageIndex: record.messageIndex,
732
+ message: JSON.parse(JSON.stringify(record.message)) as TFlexMessage,
733
+ }))
734
+ .reverse(),
735
+ ...(hasMoreArg && anchor ? {
736
+ nextCursor: this.encodeCursor({
737
+ version: 2,
738
+ kind: 'messages',
739
+ controllerId: this.controllerId,
740
+ scopeId: selectionArg.head.storageKey,
741
+ candidateId: selectionArg.manifest.candidateId,
742
+ revision: selectionArg.manifest.revision,
743
+ anchorId: anchor.message.messageId,
744
+ anchorMessageIndex: anchor.messageIndex,
745
+ sessionId: sessionIdArg,
746
+ }),
747
+ } : {}),
748
+ truncated: selectionArg.manifest.messagesTruncated
749
+ || selectionArg.manifest.messageBytesTruncated,
750
+ };
751
+ }
752
+
753
+ private encodeCursor(cursorArg: IFlexProjectionCursor): string {
754
+ return serializedBytes(cursorArg).toString('base64url');
755
+ }
756
+
757
+ private parseCursor(
758
+ cursorArg: string,
759
+ kindArg: IFlexProjectionCursor['kind'],
760
+ scopeIdArg: string,
761
+ sessionIdArg?: string,
762
+ ): IFlexProjectionCursor {
763
+ if (!isBoundedString(cursorArg, 4096)) throw new FlexProjectionCursorError();
764
+ let parsed: unknown;
765
+ try {
766
+ const bytes = Buffer.from(cursorArg, 'base64url');
767
+ if (bytes.toString('base64url') !== cursorArg) throw new Error('non-canonical');
768
+ parsed = JSON.parse(bytes.toString('utf8')) as unknown;
769
+ } catch {
770
+ throw new FlexProjectionCursorError();
771
+ }
772
+ if (
773
+ !isFlexPlainObject(parsed)
774
+ || !hasFlexExactKeys(
775
+ parsed,
776
+ [
777
+ 'version', 'kind', 'controllerId', 'scopeId', 'candidateId',
778
+ 'revision', 'anchorId',
779
+ ],
780
+ ['sessionId', 'anchorTimestamp', 'anchorMessageIndex'],
781
+ )
782
+ || parsed.version !== 2
783
+ || parsed.kind !== kindArg
784
+ || parsed.controllerId !== this.controllerId
785
+ || parsed.scopeId !== scopeIdArg
786
+ || !isBoundedString(parsed.candidateId, 64)
787
+ || !isNonNegativeInteger(parsed.revision)
788
+ || !isBoundedString(parsed.anchorId, 512)
789
+ || (kindArg === 'messages' && (
790
+ parsed.sessionId !== sessionIdArg
791
+ || !isNonNegativeInteger(parsed.anchorMessageIndex)
792
+ || parsed.anchorTimestamp !== undefined
793
+ ))
794
+ || (kindArg === 'sessions' && (
795
+ parsed.sessionId !== undefined
796
+ || !isIsoDate(parsed.anchorTimestamp)
797
+ || parsed.anchorMessageIndex !== undefined
798
+ ))
799
+ ) throw new FlexProjectionCursorError();
800
+ return parsed as unknown as IFlexProjectionCursor;
801
+ }
802
+
803
+ private findAnchorIndex<TRecord>(
804
+ recordsArg: TRecord[],
805
+ anchorIdArg: string,
806
+ anchorTimestampArg: string,
807
+ idArg: (recordArg: TRecord) => string,
808
+ timestampArg: (recordArg: TRecord) => string,
809
+ ): number {
810
+ const index = recordsArg.findIndex((record) => (
811
+ idArg(record) === anchorIdArg && timestampArg(record) === anchorTimestampArg
812
+ ));
813
+ if (index < 0) throw new FlexProjectionCursorError();
814
+ return index;
815
+ }
816
+ }