@harperfast/harper 5.2.1 → 5.2.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.
- package/bin/copyDb.ts +19 -7
- package/components/deploymentRecorder.ts +102 -11
- package/components/operations.js +4 -0
- package/config/configUtils.ts +66 -11
- package/config/harperConfigEnvVars.ts +114 -17
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dist/bin/copyDb.js +20 -7
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +6 -0
- package/dist/components/deploymentRecorder.js +94 -12
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/operations.js +4 -0
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +6 -0
- package/dist/config/configUtils.js +65 -9
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.js +108 -16
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +2 -0
- package/dist/resources/DatabaseTransaction.js +6 -5
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/Table.d.ts +2 -1
- package/dist/resources/Table.js +66 -16
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +27 -1
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/search.d.ts +1 -1
- package/dist/resources/search.js +7 -3
- package/dist/resources/search.js.map +1 -1
- package/dist/server/threads/manageThreads.js +8 -0
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/sqlEngine/parser/normalizer.js +37 -9
- package/dist/sqlEngine/parser/normalizer.js.map +1 -1
- package/dist/utility/environment/environmentManager.d.ts +11 -0
- package/dist/utility/environment/environmentManager.js +154 -0
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/lmdb/environmentUtility.js +2 -1
- package/dist/utility/lmdb/environmentUtility.js.map +1 -1
- package/npm-shrinkwrap.json +13 -7
- package/package.json +2 -2
- package/resources/DESIGN.md +1 -0
- package/resources/DatabaseTransaction.ts +7 -5
- package/resources/Table.ts +77 -16
- package/resources/auditStore.ts +32 -1
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
- package/resources/search.ts +8 -3
- package/server/threads/manageThreads.js +8 -0
- package/studio/web/assets/{Chat-aApwhRmz.js → Chat-BMUjPklt.js} +5 -5
- package/studio/web/assets/{Chat-aApwhRmz.js.map → Chat-BMUjPklt.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-sC0H91n9.js → FloatingChat-Cbghuxe5.js} +5 -5
- package/studio/web/assets/{FloatingChat-sC0H91n9.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
- package/studio/web/assets/{apiToken-DZ8JhHOI.js → apiToken-CkgTQAqc.js} +2 -2
- package/studio/web/assets/{apiToken-DZ8JhHOI.js.map → apiToken-CkgTQAqc.js.map} +1 -1
- package/studio/web/assets/{applications-9SEVLIO5.js → applications-WvF4NOTN.js} +13 -13
- package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
- package/studio/web/assets/index-CDeqVx71.css +1 -0
- package/studio/web/assets/index-Sevt3MVx.js +819 -0
- package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
- package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
- package/studio/web/assets/{index.lazy-CN1zq4I4.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
- package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
- package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
- package/studio/web/assets/{notifications-BFAF07xr.js → notifications-Ap84YyQp.js} +2 -2
- package/studio/web/assets/{notifications-BFAF07xr.js.map → notifications-Ap84YyQp.js.map} +1 -1
- package/studio/web/assets/notifications-vehqyOLM.js +2 -0
- package/studio/web/assets/{notifications-CKlYVvVN.js.map → notifications-vehqyOLM.js.map} +1 -1
- package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
- package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
- package/studio/web/assets/{profile-A1zhEdFG.js → profile-DzDpRLHY.js} +2 -2
- package/studio/web/assets/{profile-A1zhEdFG.js.map → profile-DzDpRLHY.js.map} +1 -1
- package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
- package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
- package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
- package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
- package/studio/web/assets/register-BDy01-q3.js +3 -0
- package/studio/web/assets/register-BDy01-q3.js.map +1 -0
- package/studio/web/assets/register-CQHRwNP4.js +3 -0
- package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
- package/studio/web/assets/{setComponentFile-SEtBt_GV.js → setComponentFile-D0aPcI3W.js} +2 -2
- package/studio/web/assets/{setComponentFile-SEtBt_GV.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
- package/studio/web/assets/setup-I15FAF9H.js +3 -0
- package/studio/web/assets/{setup-BisINqdH.js.map → setup-I15FAF9H.js.map} +1 -1
- package/studio/web/assets/status-BF1eJshJ.js +62 -0
- package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
- package/studio/web/assets/{swagger-ui-react-CAi_s1PC.js → swagger-ui-react-DwSPXXhl.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-CAi_s1PC.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
- package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
- package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
- package/studio/web/assets/{tsMode-DT74tlkM.js → tsMode-Bu51A-VG.js} +2 -2
- package/studio/web/assets/{tsMode-DT74tlkM.js.map → tsMode-Bu51A-VG.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-CGRGc1n7.js → useEntityRestURL-DrV1zMrN.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-CGRGc1n7.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
- package/studio/web/index.html +6 -7
- package/utility/environment/environmentManager.ts +152 -0
- package/utility/lmdb/environmentUtility.ts +2 -1
- package/studio/web/assets/applications-9SEVLIO5.js.map +0 -1
- package/studio/web/assets/index-C8Mv9kHV.css +0 -1
- package/studio/web/assets/index-Dy3uDGXb.js +0 -809
- package/studio/web/assets/index-Dy3uDGXb.js.map +0 -1
- package/studio/web/assets/index.lazy-CN1zq4I4.js +0 -14
- package/studio/web/assets/notifications-CKlYVvVN.js +0 -2
- package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
- package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
- package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
- package/studio/web/assets/setup-BisINqdH.js +0 -3
- package/studio/web/assets/status-BuQoCc7l.js +0 -62
- package/studio/web/assets/status-BuQoCc7l.js.map +0 -1
- package/studio/web/assets/textarea-DhLMZ3PA.js.map +0 -1
package/dist/resources/Table.js
CHANGED
|
@@ -341,6 +341,7 @@ function makeTable(options) {
|
|
|
341
341
|
if (!properties)
|
|
342
342
|
properties = (0, jsonSchemaTypes_ts_1.projectAttributesToProperties)(attributes);
|
|
343
343
|
const updateRecord = (0, RecordEncoder_ts_1.recordUpdater)(primaryStore, tableId, auditStore);
|
|
344
|
+
let warnedNullSourcePut = false; // latched: one warn per table per worker (see _writeUpdate)
|
|
344
345
|
let sourceLoad; // if a source has a load function (replicator), record it here
|
|
345
346
|
let hasSourceGet;
|
|
346
347
|
let primaryKeyAttribute;
|
|
@@ -2157,6 +2158,19 @@ function makeTable(options) {
|
|
|
2157
2158
|
const context = this.getContext();
|
|
2158
2159
|
const transaction = txnForContext(context);
|
|
2159
2160
|
checkValidId(id);
|
|
2161
|
+
if (fullUpdate && recordUpdate == null && options?.isNotification) {
|
|
2162
|
+
// A source/replication-applied put must carry the record; these applies skip record
|
|
2163
|
+
// validation, so this is the one path a nullish full update reaches (isNotification scopes
|
|
2164
|
+
// this to the apply dispatcher, not instance flows that fill from #changes). Applying it
|
|
2165
|
+
// stores nothing and mints an audit-only entry misrepresenting the write (#2153) — skip it;
|
|
2166
|
+
// a redelivery re-skips and a later real write supersedes.
|
|
2167
|
+
logger_ts_1.logger.trace?.('Skipped valueless source put', tableName, id, options?.nodeId);
|
|
2168
|
+
if (!warnedNullSourcePut) {
|
|
2169
|
+
warnedNullSourcePut = true;
|
|
2170
|
+
logger_ts_1.logger.warn?.(`Skipping a source-applied put with no record content for ${tableName} id ${id} from node ${options?.nodeId}`, new Error('valueless source put'));
|
|
2171
|
+
}
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2160
2174
|
const entry = this.#entry ?? primaryStore.getEntry(id, { transaction: transaction.getReadTxn() });
|
|
2161
2175
|
const writeToSource = () => {
|
|
2162
2176
|
if (!this.constructor.source || context?.source)
|
|
@@ -3186,6 +3200,13 @@ function makeTable(options) {
|
|
|
3186
3200
|
if (conditions.length > 0 || operator)
|
|
3187
3201
|
conditions = prepareConditions(conditions, operator);
|
|
3188
3202
|
const sort = typeof target.sort === 'object' && target.sort;
|
|
3203
|
+
for (let order = sort; order; order = order.next) {
|
|
3204
|
+
if (typeof order.attribute !== 'string')
|
|
3205
|
+
continue;
|
|
3206
|
+
const customIndex = indices[order.attribute]?.customIndex;
|
|
3207
|
+
if (customIndex?.exactDistance)
|
|
3208
|
+
customIndex.exactDistance(order, null);
|
|
3209
|
+
}
|
|
3189
3210
|
let postOrdering;
|
|
3190
3211
|
if (sort) {
|
|
3191
3212
|
// TODO: Support index-assisted sorts of unions, which will require potentially recursively adding/modifying an order aligned condition and be able to recursively undo it if necessary
|
|
@@ -3223,9 +3244,12 @@ function makeTable(options) {
|
|
|
3223
3244
|
if (sort.next) {
|
|
3224
3245
|
postOrdering = {
|
|
3225
3246
|
dbOrderedAttribute: sort.attribute,
|
|
3247
|
+
dbOrderedSort: sort,
|
|
3226
3248
|
attribute: sort.next.attribute,
|
|
3227
3249
|
descending: sort.next.descending,
|
|
3228
3250
|
next: sort.next.next,
|
|
3251
|
+
target: sort.next.target,
|
|
3252
|
+
distance: sort.next.distance,
|
|
3229
3253
|
};
|
|
3230
3254
|
}
|
|
3231
3255
|
}
|
|
@@ -3290,7 +3314,7 @@ function makeTable(options) {
|
|
|
3290
3314
|
// since those never run the ensureLoaded-gated freshness check this transform
|
|
3291
3315
|
// otherwise applies unconditionally to every read.
|
|
3292
3316
|
const includeExpired = target.includeExpired === true;
|
|
3293
|
-
const transformToRecord = TableResource.transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded, true, boundRowFilter, includeExpired);
|
|
3317
|
+
const transformToRecord = TableResource.transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded, true, boundRowFilter, includeExpired, postOrdering);
|
|
3294
3318
|
let results = TableResource.transformToOrderedSelect(entries, select, postOrdering, context, readTxn, transformToRecord);
|
|
3295
3319
|
// apply any offset/limit after all the sorting and filtering
|
|
3296
3320
|
if (target.offset || target.limit !== undefined)
|
|
@@ -3332,10 +3356,9 @@ function makeTable(options) {
|
|
|
3332
3356
|
function createComparator(order) {
|
|
3333
3357
|
const nextComparator = order.next && createComparator(order.next);
|
|
3334
3358
|
const descending = order.descending;
|
|
3335
|
-
context.sort = order; // make sure this is set to the current sort order
|
|
3336
3359
|
return (entryA, entryB) => {
|
|
3337
|
-
const a = getAttributeValue(entryA, order.attribute, context);
|
|
3338
|
-
const b = getAttributeValue(entryB, order.attribute, context);
|
|
3360
|
+
const a = getAttributeValue(entryA, order.attribute, context, order);
|
|
3361
|
+
const b = getAttributeValue(entryB, order.attribute, context, order);
|
|
3339
3362
|
const diff = descending
|
|
3340
3363
|
? (0, ordered_binary_1.compareKeys)(convertToComparableKeys(b), convertToComparableKeys(a))
|
|
3341
3364
|
: (0, ordered_binary_1.compareKeys)(convertToComparableKeys(a), convertToComparableKeys(b));
|
|
@@ -3385,7 +3408,7 @@ function makeTable(options) {
|
|
|
3385
3408
|
// if the index has already provided the first order of sorting, we only need to sort
|
|
3386
3409
|
// within each grouping
|
|
3387
3410
|
if (dbOrderedAttribute) {
|
|
3388
|
-
const groupingValue = getAttributeValue(entry, dbOrderedAttribute, context);
|
|
3411
|
+
const groupingValue = getAttributeValue(entry, dbOrderedAttribute, context, sort.dbOrderedSort);
|
|
3389
3412
|
if (firstEntry) {
|
|
3390
3413
|
firstEntry = false;
|
|
3391
3414
|
lastGroupingValue = groupingValue;
|
|
@@ -3495,9 +3518,10 @@ function makeTable(options) {
|
|
|
3495
3518
|
* authorization verdict can't be made on bytes that differ from what the caller receives.
|
|
3496
3519
|
* @param includeExpired when true, a row past its TTL but not yet swept is treated as a live
|
|
3497
3520
|
* match rather than gone (used by the SQL engine's UPDATE/DELETE row-finder).
|
|
3521
|
+
* @param sort post-ordering owned by this selection
|
|
3498
3522
|
* @returns
|
|
3499
3523
|
*/
|
|
3500
|
-
static transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded, canSkip, rowFilter, includeExpired) {
|
|
3524
|
+
static transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded, canSkip, rowFilter, includeExpired, sort) {
|
|
3501
3525
|
let checkLoaded;
|
|
3502
3526
|
if (ensure_loaded &&
|
|
3503
3527
|
hasSourceGet &&
|
|
@@ -3566,7 +3590,7 @@ function makeTable(options) {
|
|
|
3566
3590
|
swrResource.setRecord(record);
|
|
3567
3591
|
const loadingFromSource = ensureLoadedFromSource(source, entry.key ?? entry, entry, context, swrResource);
|
|
3568
3592
|
if (loadingFromSource?.then) {
|
|
3569
|
-
return loadingFromSource.then(transform);
|
|
3593
|
+
return loadingFromSource.then(transform.bind(this));
|
|
3570
3594
|
}
|
|
3571
3595
|
}
|
|
3572
3596
|
}
|
|
@@ -3603,7 +3627,7 @@ function makeTable(options) {
|
|
|
3603
3627
|
}
|
|
3604
3628
|
}
|
|
3605
3629
|
else {
|
|
3606
|
-
value = resolver(record, context, entry, true);
|
|
3630
|
+
value = resolver(record, context, entry, true, sort);
|
|
3607
3631
|
}
|
|
3608
3632
|
const handleResolvedValue = (value) => {
|
|
3609
3633
|
if (resolver.directReturn)
|
|
@@ -3621,7 +3645,7 @@ function makeTable(options) {
|
|
|
3621
3645
|
// otherwise pass along the nested selected
|
|
3622
3646
|
attribute_name === attribute
|
|
3623
3647
|
? null
|
|
3624
|
-
: attribute.select || (Array.isArray(attribute) ? attribute : null), context, targetReadTxn, filterMap, ensure_loaded));
|
|
3648
|
+
: attribute.select || (Array.isArray(attribute) ? attribute : null), context, targetReadTxn, filterMap, ensure_loaded, undefined, undefined, undefined, typeof attribute.sort === 'object' && attribute.sort));
|
|
3625
3649
|
if (Array.isArray(value)) {
|
|
3626
3650
|
const results = [];
|
|
3627
3651
|
const iterator = targetTable
|
|
@@ -4679,8 +4703,26 @@ function makeTable(options) {
|
|
|
4679
4703
|
$updatedTime: (object, context, entry) => entry.version,
|
|
4680
4704
|
$expiresAt: (object, context, entry) => entry.expiresAt,
|
|
4681
4705
|
$record: (object, context, entry) => (entry ? { value: object } : object),
|
|
4682
|
-
$distance: (object, context, entry) => {
|
|
4683
|
-
|
|
4706
|
+
$distance: (object, context, entry, returnEntry, sort) => {
|
|
4707
|
+
if (!entry)
|
|
4708
|
+
return;
|
|
4709
|
+
if (entry.distance !== undefined)
|
|
4710
|
+
return entry.distance;
|
|
4711
|
+
let distanceSort = sort;
|
|
4712
|
+
while (distanceSort &&
|
|
4713
|
+
(typeof distanceSort.attribute !== 'string' ||
|
|
4714
|
+
!Array.isArray(distanceSort.target) ||
|
|
4715
|
+
!indices[distanceSort.attribute]?.customIndex?.propertyResolver))
|
|
4716
|
+
distanceSort = distanceSort.next;
|
|
4717
|
+
if (!distanceSort)
|
|
4718
|
+
return;
|
|
4719
|
+
const customIndex = indices[distanceSort.attribute].customIndex;
|
|
4720
|
+
const vector = object[distanceSort.attribute];
|
|
4721
|
+
const distanceCache = context?.vectorDistanceCaches?.get(distanceSort);
|
|
4722
|
+
const cachedDistance = distanceCache?.get(entry) ?? distanceCache?.get(vector);
|
|
4723
|
+
if (cachedDistance !== undefined)
|
|
4724
|
+
return cachedDistance;
|
|
4725
|
+
return customIndex.propertyResolver(vector, context, entry, distanceSort);
|
|
4684
4726
|
},
|
|
4685
4727
|
};
|
|
4686
4728
|
for (const attribute of this.attributes) {
|
|
@@ -4820,9 +4862,13 @@ function makeTable(options) {
|
|
|
4820
4862
|
}
|
|
4821
4863
|
else if (indices[attribute.name]?.customIndex?.propertyResolver) {
|
|
4822
4864
|
const customIndex = indices[attribute.name].customIndex;
|
|
4823
|
-
propertyResolvers[attribute.name] = (object, context, entry) => {
|
|
4865
|
+
propertyResolvers[attribute.name] = (object, context, entry, returnEntry, sort, comparing) => {
|
|
4824
4866
|
const value = object[attribute.name];
|
|
4825
|
-
|
|
4867
|
+
const sortAttribute = sort?.attribute;
|
|
4868
|
+
const resolvesSort = comparing === true &&
|
|
4869
|
+
(sortAttribute === attribute.name ||
|
|
4870
|
+
(Array.isArray(sortAttribute) && sortAttribute[sortAttribute.length - 1] === attribute.name));
|
|
4871
|
+
return customIndex.propertyResolver(value, context, entry, resolvesSort ? sort : undefined);
|
|
4826
4872
|
};
|
|
4827
4873
|
propertyResolvers[attribute.name].directReturn = true;
|
|
4828
4874
|
}
|
|
@@ -5386,6 +5432,7 @@ function makeTable(options) {
|
|
|
5386
5432
|
// Inherit never-drop-on-conflict so a source-applied multi-store transaction doesn't
|
|
5387
5433
|
// drop the canonical write when a secondary store hits a transient conflict.
|
|
5388
5434
|
transaction.next.sourceApply = transaction.sourceApply;
|
|
5435
|
+
transaction.next.timeoutBudget = transaction.timeoutBudget;
|
|
5389
5436
|
// Inherit the replay marker so a multi-table replay transaction skips validation on
|
|
5390
5437
|
// every store, not just the first (harper#1316).
|
|
5391
5438
|
transaction.next.isReplay = transaction.isReplay;
|
|
@@ -5417,7 +5464,7 @@ function makeTable(options) {
|
|
|
5417
5464
|
return transaction;
|
|
5418
5465
|
}
|
|
5419
5466
|
}
|
|
5420
|
-
function getAttributeValue(entry, attribute_name, context) {
|
|
5467
|
+
function getAttributeValue(entry, attribute_name, context, sort) {
|
|
5421
5468
|
if (!entry) {
|
|
5422
5469
|
return;
|
|
5423
5470
|
}
|
|
@@ -5429,14 +5476,17 @@ function makeTable(options) {
|
|
|
5429
5476
|
for (let i = 0, l = attribute_name.length; i < l; i++) {
|
|
5430
5477
|
const attribute = attribute_name[i];
|
|
5431
5478
|
const resolver = resolvers?.[attribute];
|
|
5432
|
-
value =
|
|
5479
|
+
value =
|
|
5480
|
+
resolver && value
|
|
5481
|
+
? resolver(value, context, entry, false, i === l - 1 ? sort : undefined, true)
|
|
5482
|
+
: value?.[attribute];
|
|
5433
5483
|
entry = null; // can't use this in the nested object
|
|
5434
5484
|
resolvers = resolver?.definition?.tableClass?.propertyResolvers;
|
|
5435
5485
|
}
|
|
5436
5486
|
return value;
|
|
5437
5487
|
}
|
|
5438
5488
|
const resolver = propertyResolvers[attribute_name];
|
|
5439
|
-
return resolver ? resolver(record, context, entry) : record[attribute_name];
|
|
5489
|
+
return resolver ? resolver(record, context, entry, false, sort, true) : record[attribute_name];
|
|
5440
5490
|
}
|
|
5441
5491
|
function transformToEntries(ids, select, context, readTxn, filters) {
|
|
5442
5492
|
// TODO: Test and ensure that we break out of these loops when a connection is lost
|