@tanstack/electric-db-collection 0.2.43 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/electric.cjs +62 -4
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/tag-index.cjs +31 -4
- package/dist/cjs/tag-index.cjs.map +1 -1
- package/dist/cjs/tag-index.d.cts +39 -10
- package/dist/esm/electric.js +68 -10
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/tag-index.d.ts +39 -10
- package/dist/esm/tag-index.js +31 -4
- package/dist/esm/tag-index.js.map +1 -1
- package/package.json +3 -3
- package/src/electric.ts +107 -11
- package/src/tag-index.ts +76 -17
package/src/electric.ts
CHANGED
|
@@ -16,15 +16,21 @@ import {
|
|
|
16
16
|
import { compileSQL } from './sql-compiler'
|
|
17
17
|
import {
|
|
18
18
|
addTagToIndex,
|
|
19
|
+
deriveDisjunctPositions,
|
|
19
20
|
findRowsMatchingPattern,
|
|
20
21
|
getTagLength,
|
|
22
|
+
isMoveInMessage,
|
|
21
23
|
isMoveOutMessage,
|
|
24
|
+
parseTag as parseTagString,
|
|
22
25
|
removeTagFromIndex,
|
|
26
|
+
rowVisible,
|
|
23
27
|
tagMatchesPattern,
|
|
24
28
|
} from './tag-index'
|
|
25
29
|
import type { ColumnEncoder } from './sql-compiler'
|
|
26
30
|
import type {
|
|
27
|
-
|
|
31
|
+
ActiveConditions,
|
|
32
|
+
DisjunctPositions,
|
|
33
|
+
MovePattern,
|
|
28
34
|
MoveTag,
|
|
29
35
|
ParsedMoveTag,
|
|
30
36
|
RowId,
|
|
@@ -981,16 +987,16 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
981
987
|
|
|
982
988
|
const tagCache = new Map<MoveTag, ParsedMoveTag>()
|
|
983
989
|
|
|
984
|
-
// Parses a tag string into a
|
|
990
|
+
// Parses a tag string into a ParsedMoveTag.
|
|
985
991
|
// It memoizes the result parsed tag such that future calls
|
|
986
|
-
// for the same tag string return the same
|
|
992
|
+
// for the same tag string return the same ParsedMoveTag array.
|
|
987
993
|
const parseTag = (tag: MoveTag): ParsedMoveTag => {
|
|
988
994
|
const cachedTag = tagCache.get(tag)
|
|
989
995
|
if (cachedTag) {
|
|
990
996
|
return cachedTag
|
|
991
997
|
}
|
|
992
998
|
|
|
993
|
-
const parsedTag = tag
|
|
999
|
+
const parsedTag = parseTagString(tag)
|
|
994
1000
|
tagCache.set(tag, parsedTag)
|
|
995
1001
|
return parsedTag
|
|
996
1002
|
}
|
|
@@ -1000,6 +1006,11 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1000
1006
|
const tagIndex: TagIndex = []
|
|
1001
1007
|
let tagLength: number | undefined = undefined
|
|
1002
1008
|
|
|
1009
|
+
// DNF state: active_conditions are per-row, disjunct_positions are global
|
|
1010
|
+
// (fixed by the shape's WHERE clause, derived once from the first tagged message).
|
|
1011
|
+
const rowActiveConditions = new Map<RowId, ActiveConditions>()
|
|
1012
|
+
let disjunctPositions: DisjunctPositions | undefined = undefined
|
|
1013
|
+
|
|
1003
1014
|
/**
|
|
1004
1015
|
* Initialize the tag index with the correct length
|
|
1005
1016
|
*/
|
|
@@ -1074,6 +1085,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1074
1085
|
tags: Array<MoveTag> | undefined,
|
|
1075
1086
|
removedTags: Array<MoveTag> | undefined,
|
|
1076
1087
|
rowId: RowId,
|
|
1088
|
+
activeConditions?: ActiveConditions,
|
|
1077
1089
|
): Set<MoveTag> => {
|
|
1078
1090
|
// Initialize tag set for this row if it doesn't exist (needed for checking deletion)
|
|
1079
1091
|
if (!rowTagSets.has(rowId)) {
|
|
@@ -1084,6 +1096,12 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1084
1096
|
// Add new tags
|
|
1085
1097
|
if (tags) {
|
|
1086
1098
|
addTagsToRow(tags, rowId, rowTagSet)
|
|
1099
|
+
|
|
1100
|
+
// Derive disjunct positions once — they are fixed by the shape's WHERE clause.
|
|
1101
|
+
if (disjunctPositions === undefined) {
|
|
1102
|
+
const parsedTags = tags.map(parseTag)
|
|
1103
|
+
disjunctPositions = deriveDisjunctPositions(parsedTags)
|
|
1104
|
+
}
|
|
1087
1105
|
}
|
|
1088
1106
|
|
|
1089
1107
|
// Remove tags
|
|
@@ -1091,6 +1109,11 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1091
1109
|
removeTagsFromRow(removedTags, rowId, rowTagSet)
|
|
1092
1110
|
}
|
|
1093
1111
|
|
|
1112
|
+
// Store active conditions if provided (overwrite on re-send)
|
|
1113
|
+
if (activeConditions && activeConditions.length > 0) {
|
|
1114
|
+
rowActiveConditions.set(rowId, [...activeConditions])
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1094
1117
|
return rowTagSet
|
|
1095
1118
|
}
|
|
1096
1119
|
|
|
@@ -1101,6 +1124,8 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1101
1124
|
rowTagSets.clear()
|
|
1102
1125
|
tagIndex.length = 0
|
|
1103
1126
|
tagLength = undefined
|
|
1127
|
+
rowActiveConditions.clear()
|
|
1128
|
+
disjunctPositions = undefined
|
|
1104
1129
|
}
|
|
1105
1130
|
|
|
1106
1131
|
/**
|
|
@@ -1129,22 +1154,45 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1129
1154
|
|
|
1130
1155
|
// Remove the row from the tag sets map
|
|
1131
1156
|
rowTagSets.delete(rowId)
|
|
1157
|
+
rowActiveConditions.delete(rowId)
|
|
1132
1158
|
}
|
|
1133
1159
|
|
|
1134
1160
|
/**
|
|
1135
1161
|
* Remove matching tags from a row based on a pattern
|
|
1136
|
-
* Returns true if the row
|
|
1162
|
+
* Returns true if the row should be deleted (no longer visible)
|
|
1137
1163
|
*/
|
|
1138
1164
|
const removeMatchingTagsFromRow = (
|
|
1139
1165
|
rowId: RowId,
|
|
1140
|
-
pattern:
|
|
1166
|
+
pattern: MovePattern,
|
|
1141
1167
|
): boolean => {
|
|
1142
1168
|
const rowTagSet = rowTagSets.get(rowId)
|
|
1143
1169
|
if (!rowTagSet) {
|
|
1144
1170
|
return false
|
|
1145
1171
|
}
|
|
1146
1172
|
|
|
1147
|
-
//
|
|
1173
|
+
// DNF mode: check visibility using active conditions.
|
|
1174
|
+
// Tag index entries are preserved so that move-in can re-activate positions.
|
|
1175
|
+
const activeConditions = rowActiveConditions.get(rowId)
|
|
1176
|
+
if (activeConditions && disjunctPositions) {
|
|
1177
|
+
// Set the condition at this pattern's position to false
|
|
1178
|
+
activeConditions[pattern.pos] = false
|
|
1179
|
+
|
|
1180
|
+
if (!rowVisible(activeConditions, disjunctPositions)) {
|
|
1181
|
+
// Row is no longer visible — clean up all state including tag index
|
|
1182
|
+
for (const tag of rowTagSet) {
|
|
1183
|
+
const parsedTag = parseTag(tag)
|
|
1184
|
+
removeTagFromIndex(parsedTag, rowId, tagIndex, tagLength!)
|
|
1185
|
+
tagCache.delete(tag)
|
|
1186
|
+
}
|
|
1187
|
+
rowTagSets.delete(rowId)
|
|
1188
|
+
rowActiveConditions.delete(rowId)
|
|
1189
|
+
return true
|
|
1190
|
+
}
|
|
1191
|
+
return false
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Simple shape (no subquery dependencies — server sends no active_conditions):
|
|
1195
|
+
// Remove matching tags and delete if tag set is empty
|
|
1148
1196
|
for (const tag of rowTagSet) {
|
|
1149
1197
|
const parsedTag = parseTag(tag)
|
|
1150
1198
|
if (tagMatchesPattern(parsedTag, pattern)) {
|
|
@@ -1153,7 +1201,6 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1153
1201
|
}
|
|
1154
1202
|
}
|
|
1155
1203
|
|
|
1156
|
-
// Check if row's tag set is now empty
|
|
1157
1204
|
if (rowTagSet.size === 0) {
|
|
1158
1205
|
rowTagSets.delete(rowId)
|
|
1159
1206
|
return true
|
|
@@ -1166,7 +1213,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1166
1213
|
* Process move-out event: remove matching tags from rows and delete rows with empty tag sets
|
|
1167
1214
|
*/
|
|
1168
1215
|
const processMoveOutEvent = (
|
|
1169
|
-
patterns: Array<
|
|
1216
|
+
patterns: Array<MovePattern>,
|
|
1170
1217
|
begin: () => void,
|
|
1171
1218
|
write: (message: ChangeMessageOrDeleteKeyMessage<T>) => void,
|
|
1172
1219
|
transactionStarted: boolean,
|
|
@@ -1204,6 +1251,30 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1204
1251
|
return txStarted
|
|
1205
1252
|
}
|
|
1206
1253
|
|
|
1254
|
+
/**
|
|
1255
|
+
* Process move-in event: re-activate conditions for rows matching the patterns.
|
|
1256
|
+
* This is a silent operation — no messages are emitted to the collection.
|
|
1257
|
+
*/
|
|
1258
|
+
const processMoveInEvent = (patterns: Array<MovePattern>): void => {
|
|
1259
|
+
if (tagLength === undefined) {
|
|
1260
|
+
debug(
|
|
1261
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received move-in message but no tag length set yet, ignoring`,
|
|
1262
|
+
)
|
|
1263
|
+
return
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
for (const pattern of patterns) {
|
|
1267
|
+
const affectedRowIds = findRowsMatchingPattern(pattern, tagIndex)
|
|
1268
|
+
|
|
1269
|
+
for (const rowId of affectedRowIds) {
|
|
1270
|
+
const activeConditions = rowActiveConditions.get(rowId)
|
|
1271
|
+
if (activeConditions) {
|
|
1272
|
+
activeConditions[pattern.pos] = true
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1207
1278
|
/**
|
|
1208
1279
|
* Get the sync metadata for insert operations
|
|
1209
1280
|
* @returns Record containing relation information
|
|
@@ -1433,6 +1504,11 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1433
1504
|
const removedTags = changeMessage.headers.removed_tags
|
|
1434
1505
|
const hasTags = tags || removedTags
|
|
1435
1506
|
|
|
1507
|
+
// Extract active_conditions from headers (DNF support)
|
|
1508
|
+
const activeConditions = changeMessage.headers.active_conditions as
|
|
1509
|
+
| ActiveConditions
|
|
1510
|
+
| undefined
|
|
1511
|
+
|
|
1436
1512
|
const rowId = collection.getKeyFromItem(changeMessage.value)
|
|
1437
1513
|
const operation = changeMessage.headers.operation
|
|
1438
1514
|
|
|
@@ -1453,7 +1529,12 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1453
1529
|
if (isDelete) {
|
|
1454
1530
|
clearTagsForRow(rowId)
|
|
1455
1531
|
} else if (hasTags) {
|
|
1456
|
-
processTagsForChangeMessage(
|
|
1532
|
+
processTagsForChangeMessage(
|
|
1533
|
+
tags,
|
|
1534
|
+
removedTags,
|
|
1535
|
+
rowId,
|
|
1536
|
+
activeConditions,
|
|
1537
|
+
)
|
|
1457
1538
|
}
|
|
1458
1539
|
|
|
1459
1540
|
write({
|
|
@@ -1496,7 +1577,11 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1496
1577
|
|
|
1497
1578
|
for (const message of messages) {
|
|
1498
1579
|
// Add message to current batch buffer (for race condition handling)
|
|
1499
|
-
if (
|
|
1580
|
+
if (
|
|
1581
|
+
isChangeMessage(message) ||
|
|
1582
|
+
isMoveOutMessage(message) ||
|
|
1583
|
+
isMoveInMessage(message)
|
|
1584
|
+
) {
|
|
1500
1585
|
currentBatchMessages.setState((currentBuffer) => {
|
|
1501
1586
|
const newBuffer = [...currentBuffer, message]
|
|
1502
1587
|
// Limit buffer size for safety
|
|
@@ -1593,6 +1678,14 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1593
1678
|
transactionStarted,
|
|
1594
1679
|
)
|
|
1595
1680
|
}
|
|
1681
|
+
} else if (isMoveInMessage(message)) {
|
|
1682
|
+
// Handle move-in event: re-activate conditions for matching rows.
|
|
1683
|
+
// Buffer if buffering, otherwise process immediately.
|
|
1684
|
+
if (isBufferingInitialSync() && !transactionStarted) {
|
|
1685
|
+
bufferedMessages.push(message)
|
|
1686
|
+
} else {
|
|
1687
|
+
processMoveInEvent(message.headers.patterns)
|
|
1688
|
+
}
|
|
1596
1689
|
} else if (isMustRefetchMessage(message)) {
|
|
1597
1690
|
debug(
|
|
1598
1691
|
`${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`,
|
|
@@ -1672,6 +1765,9 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1672
1765
|
write,
|
|
1673
1766
|
transactionStarted,
|
|
1674
1767
|
)
|
|
1768
|
+
} else if (isMoveInMessage(bufferedMsg)) {
|
|
1769
|
+
// Process buffered move-in messages during atomic swap
|
|
1770
|
+
processMoveInEvent(bufferedMsg.headers.patterns)
|
|
1675
1771
|
}
|
|
1676
1772
|
}
|
|
1677
1773
|
|
package/src/tag-index.ts
CHANGED
|
@@ -3,23 +3,32 @@ import type { Message, Row } from '@electric-sql/client'
|
|
|
3
3
|
|
|
4
4
|
export type RowId = string | number
|
|
5
5
|
export type MoveTag = string
|
|
6
|
-
export type ParsedMoveTag = Array<string>
|
|
6
|
+
export type ParsedMoveTag = Array<string | NonParticipating>
|
|
7
7
|
export type Position = number
|
|
8
8
|
export type Value = string
|
|
9
|
-
export type
|
|
9
|
+
export type MovePattern = {
|
|
10
10
|
pos: Position
|
|
11
11
|
value: Value
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Sentinel value for tag positions where the disjunct does not participate
|
|
16
|
+
* in that condition. These positions are not indexed and won't match any
|
|
17
|
+
* move pattern.
|
|
18
|
+
*/
|
|
19
|
+
export const NON_PARTICIPATING = null
|
|
20
|
+
export type NonParticipating = typeof NON_PARTICIPATING
|
|
21
|
+
|
|
22
|
+
export type ActiveConditions = Array<boolean>
|
|
23
|
+
export type DisjunctPositions = Array<Array<number>>
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
|
-
* Event message type for move-out events
|
|
26
|
+
* Event message type for move-out and move-in events
|
|
18
27
|
*/
|
|
19
28
|
export interface EventMessage {
|
|
20
29
|
headers: {
|
|
21
|
-
event: `move-out`
|
|
22
|
-
patterns: Array<
|
|
30
|
+
event: `move-out` | `move-in`
|
|
31
|
+
patterns: Array<MovePattern>
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -41,10 +50,21 @@ export interface EventMessage {
|
|
|
41
50
|
*/
|
|
42
51
|
export type TagIndex = Array<Map<Value, Set<RowId>>>
|
|
43
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Parse a tag string into a ParsedMoveTag.
|
|
55
|
+
* Splits on `/` delimiter and maps empty strings to {@link NON_PARTICIPATING}.
|
|
56
|
+
*/
|
|
57
|
+
export function parseTag(tag: MoveTag): ParsedMoveTag {
|
|
58
|
+
return tag.split(`/`).map((s) => (s === `` ? NON_PARTICIPATING : s))
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
/**
|
|
45
62
|
* Abstraction to get the value at a specific position in a tag
|
|
46
63
|
*/
|
|
47
|
-
export function getValue(
|
|
64
|
+
export function getValue(
|
|
65
|
+
tag: ParsedMoveTag,
|
|
66
|
+
position: Position,
|
|
67
|
+
): string | NonParticipating {
|
|
48
68
|
if (position >= tag.length) {
|
|
49
69
|
throw new Error(`Position out of bounds`)
|
|
50
70
|
}
|
|
@@ -54,7 +74,7 @@ export function getValue(tag: ParsedMoveTag, position: Position): Value {
|
|
|
54
74
|
/**
|
|
55
75
|
* Abstraction to extract position and value from a pattern.
|
|
56
76
|
*/
|
|
57
|
-
function getPositionalValue(pattern:
|
|
77
|
+
function getPositionalValue(pattern: MovePattern): {
|
|
58
78
|
pos: number
|
|
59
79
|
value: string
|
|
60
80
|
} {
|
|
@@ -70,16 +90,16 @@ export function getTagLength(tag: ParsedMoveTag): number {
|
|
|
70
90
|
|
|
71
91
|
/**
|
|
72
92
|
* Check if a tag matches a pattern.
|
|
73
|
-
* A tag matches if the value at the pattern's position equals the pattern's value
|
|
74
|
-
*
|
|
93
|
+
* A tag matches if the value at the pattern's position equals the pattern's value.
|
|
94
|
+
* {@link NON_PARTICIPATING} positions naturally don't match any string value.
|
|
75
95
|
*/
|
|
76
96
|
export function tagMatchesPattern(
|
|
77
97
|
tag: ParsedMoveTag,
|
|
78
|
-
pattern:
|
|
98
|
+
pattern: MovePattern,
|
|
79
99
|
): boolean {
|
|
80
100
|
const { pos, value } = getPositionalValue(pattern)
|
|
81
101
|
const tagValue = getValue(tag, pos)
|
|
82
|
-
return tagValue === value
|
|
102
|
+
return tagValue === value
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
/**
|
|
@@ -94,8 +114,7 @@ export function addTagToIndex(
|
|
|
94
114
|
for (let i = 0; i < tagLength; i++) {
|
|
95
115
|
const value = getValue(tag, i)
|
|
96
116
|
|
|
97
|
-
|
|
98
|
-
if (value !== TAG_WILDCARD) {
|
|
117
|
+
if (value !== NON_PARTICIPATING) {
|
|
99
118
|
const positionIndex = index[i]!
|
|
100
119
|
if (!positionIndex.has(value)) {
|
|
101
120
|
positionIndex.set(value, new Set())
|
|
@@ -119,8 +138,7 @@ export function removeTagFromIndex(
|
|
|
119
138
|
for (let i = 0; i < tagLength; i++) {
|
|
120
139
|
const value = getValue(tag, i)
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
if (value !== TAG_WILDCARD) {
|
|
141
|
+
if (value !== NON_PARTICIPATING) {
|
|
124
142
|
const positionIndex = index[i]
|
|
125
143
|
if (positionIndex) {
|
|
126
144
|
const rowSet = positionIndex.get(value)
|
|
@@ -141,7 +159,7 @@ export function removeTagFromIndex(
|
|
|
141
159
|
* Find all rows that match a given pattern
|
|
142
160
|
*/
|
|
143
161
|
export function findRowsMatchingPattern(
|
|
144
|
-
pattern:
|
|
162
|
+
pattern: MovePattern,
|
|
145
163
|
index: TagIndex,
|
|
146
164
|
): Set<RowId> {
|
|
147
165
|
const { pos, value } = getPositionalValue(pattern)
|
|
@@ -150,6 +168,38 @@ export function findRowsMatchingPattern(
|
|
|
150
168
|
return rowSet ?? new Set()
|
|
151
169
|
}
|
|
152
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Derive disjunct positions from parsed tags.
|
|
173
|
+
* For each tag (= disjunct), collect the indices of participating positions.
|
|
174
|
+
* E.g., ["hash_a", NON_PARTICIPATING, "hash_b"] → [0, 2]
|
|
175
|
+
*/
|
|
176
|
+
export function deriveDisjunctPositions(
|
|
177
|
+
tags: Array<ParsedMoveTag>,
|
|
178
|
+
): DisjunctPositions {
|
|
179
|
+
return tags.map((tag) => {
|
|
180
|
+
const positions: Array<number> = []
|
|
181
|
+
for (let i = 0; i < tag.length; i++) {
|
|
182
|
+
if (tag[i] !== NON_PARTICIPATING) {
|
|
183
|
+
positions.push(i)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return positions
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Evaluate whether a row is visible given active conditions and disjunct positions.
|
|
192
|
+
* Returns true if ANY disjunct has ALL its positions as true in activeConditions.
|
|
193
|
+
*/
|
|
194
|
+
export function rowVisible(
|
|
195
|
+
activeConditions: ActiveConditions,
|
|
196
|
+
disjunctPositions: DisjunctPositions,
|
|
197
|
+
): boolean {
|
|
198
|
+
return disjunctPositions.some((positions) =>
|
|
199
|
+
positions.every((pos) => activeConditions[pos]),
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
153
203
|
/**
|
|
154
204
|
* Check if a message is an event message with move-out event
|
|
155
205
|
*/
|
|
@@ -158,3 +208,12 @@ export function isMoveOutMessage<T extends Row<unknown>>(
|
|
|
158
208
|
): message is Message<T> & EventMessage {
|
|
159
209
|
return message.headers.event === `move-out`
|
|
160
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if a message is an event message with move-in event
|
|
214
|
+
*/
|
|
215
|
+
export function isMoveInMessage<T extends Row<unknown>>(
|
|
216
|
+
message: Message<T>,
|
|
217
|
+
): message is Message<T> & EventMessage {
|
|
218
|
+
return message.headers.event === `move-in`
|
|
219
|
+
}
|