Skip to content

Commit 7b2fc59

Browse files
committed
Implementing new structures for DateTime
The structures with signature `0x46` and `0x66` are being replaced by `0x49` and `0x69`. This new structures changes the meaning of seconds and nano seconds from `adjusted Unix epoch` to `UTC`. This changes have with goal of avoiding unexistent or ambiguos ZonedDateTime to be received or sent over Bolt. Bolt v4.3 and v4.4 were patched to support this feature if the server supports the patch. This is a backport of neo4j#948
1 parent 1a1c11c commit 7b2fc59

File tree

12 files changed

+678
-30
lines changed

12 files changed

+678
-30
lines changed

packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import BoltProtocolV42 from './bolt-protocol-v4x2'
2020
import RequestMessage from './request-message'
2121
import { RouteObserver } from './stream-observers'
22+
import RequestMessage from './request-message'
23+
import { LoginObserver } from './stream-observers'
2224

2325
import { internal } from 'neo4j-driver-core'
2426

@@ -65,4 +67,49 @@ export default class BoltProtocol extends BoltProtocolV42 {
6567

6668
return observer
6769
}
70+
71+
/**
72+
* Initialize a connection with the server
73+
*
74+
* @param {Object} param0 The params
75+
* @param {string} param0.userAgent The user agent
76+
* @param {any} param0.authToken The auth token
77+
* @param {function(error)} param0.onError On error callback
78+
* @param {function(onComplte)} param0.onComplete On complete callback
79+
* @returns {LoginObserver} The Login observer
80+
*/
81+
initialize ({ userAgent, authToken, onError, onComplete } = {}) {
82+
const observer = new LoginObserver({
83+
onError: error => this._onLoginError(error, onError),
84+
onCompleted: metadata => {
85+
if (metadata.patch_bolt !== undefined) {
86+
this._applyPatches(metadata.patch_bolt)
87+
}
88+
return this._onLoginCompleted(metadata, onComplete)
89+
}
90+
})
91+
92+
this.write(
93+
RequestMessage.hello(userAgent, authToken, this._serversideRouting, ['utc']),
94+
observer,
95+
true
96+
)
97+
98+
return observer
99+
}
100+
101+
/**
102+
*
103+
* @param {string[]} patches Patches to be applied to the protocol
104+
*/
105+
_applyPatches (patches) {
106+
if (patches.includes('utc')) {
107+
this._applyUtcPatch()
108+
}
109+
}
110+
111+
_applyUtcPatch () {
112+
this._packer.useUtc = true
113+
this._unpacker.useUtc = true
114+
}
68115
}

packages/bolt-connection/src/bolt/request-message.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,14 @@ export default class RequestMessage {
106106
* @param {Object} optional server side routing, set to routing context to turn on server side routing (> 4.1)
107107
* @return {RequestMessage} new HELLO message.
108108
*/
109-
static hello (userAgent, authToken, routing = null) {
109+
static hello (userAgent, authToken, routing = null, patchs = null) {
110110
const metadata = Object.assign({ user_agent: userAgent }, authToken)
111111
if (routing) {
112112
metadata.routing = routing
113113
}
114+
if (patchs) {
115+
metadata.patch_bolt = patchs
116+
}
114117
return new RequestMessage(
115118
HELLO,
116119
[metadata],
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import {
21+
DateTime,
22+
isInt,
23+
int,
24+
internal
25+
} from 'neo4j-driver-core'
26+
27+
28+
import {
29+
epochSecondAndNanoToLocalDateTime
30+
} from './temporal-factory'
31+
32+
const {
33+
temporalUtil: {
34+
localDateTimeToEpochSecond
35+
}
36+
} = internal
37+
38+
export const DATE_TIME_WITH_ZONE_OFFSET = 0x49
39+
const DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE = 3
40+
41+
export const DATE_TIME_WITH_ZONE_ID = 0x69
42+
const DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE = 3
43+
44+
/**
45+
* Unpack date time with zone offset value using the given unpacker.
46+
* @param {Unpacker} unpacker the unpacker to use.
47+
* @param {number} structSize the retrieved struct size.
48+
* @param {BaseBuffer} buffer the buffer to unpack from.
49+
* @param {boolean} disableLosslessIntegers if integer properties in the result date-time should be native JS numbers.
50+
* @return {DateTime} the unpacked date time with zone offset value.
51+
*/
52+
export function unpackDateTimeWithZoneOffset (
53+
unpacker,
54+
structSize,
55+
buffer,
56+
disableLosslessIntegers,
57+
useBigInt
58+
) {
59+
unpacker._verifyStructSize(
60+
'DateTimeWithZoneOffset',
61+
DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE,
62+
structSize
63+
)
64+
65+
const utcSecond = unpacker.unpackInteger(buffer)
66+
const nano = unpacker.unpackInteger(buffer)
67+
const timeZoneOffsetSeconds = unpacker.unpackInteger(buffer)
68+
69+
const epochSecond = int(utcSecond).add(timeZoneOffsetSeconds)
70+
const localDateTime = epochSecondAndNanoToLocalDateTime(epochSecond, nano)
71+
const result = new DateTime(
72+
localDateTime.year,
73+
localDateTime.month,
74+
localDateTime.day,
75+
localDateTime.hour,
76+
localDateTime.minute,
77+
localDateTime.second,
78+
localDateTime.nanosecond,
79+
timeZoneOffsetSeconds,
80+
null
81+
)
82+
return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt)
83+
}
84+
85+
/**
86+
* Unpack date time with zone id value using the given unpacker.
87+
* @param {Unpacker} unpacker the unpacker to use.
88+
* @param {number} structSize the retrieved struct size.
89+
* @param {BaseBuffer} buffer the buffer to unpack from.
90+
* @param {boolean} disableLosslessIntegers if integer properties in the result date-time should be native JS numbers.
91+
* @return {DateTime} the unpacked date time with zone id value.
92+
*/
93+
export function unpackDateTimeWithZoneId (
94+
unpacker,
95+
structSize,
96+
buffer,
97+
disableLosslessIntegers,
98+
useBigInt
99+
) {
100+
unpacker._verifyStructSize(
101+
'DateTimeWithZoneId',
102+
DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE,
103+
structSize
104+
)
105+
106+
const epochSecond = unpacker.unpackInteger(buffer)
107+
const nano = unpacker.unpackInteger(buffer)
108+
const timeZoneId = unpacker.unpack(buffer)
109+
110+
const localDateTime = getTimeInZoneId(timeZoneId, epochSecond, nano)
111+
112+
const result = new DateTime(
113+
localDateTime.year,
114+
localDateTime.month,
115+
localDateTime.day,
116+
localDateTime.hour,
117+
localDateTime.minute,
118+
localDateTime.second,
119+
int(nano),
120+
localDateTime.timeZoneOffsetSeconds,
121+
timeZoneId
122+
)
123+
return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt)
124+
}
125+
126+
/*
127+
* Pack given date time.
128+
* @param {DateTime} value the date time value to pack.
129+
* @param {Packer} packer the packer to use.
130+
*/
131+
export function packDateTime (value, packer) {
132+
if (value.timeZoneId) {
133+
packDateTimeWithZoneId(value, packer)
134+
} else {
135+
packDateTimeWithZoneOffset(value, packer)
136+
}
137+
}
138+
139+
/**
140+
* Pack given date time with zone id.
141+
* @param {DateTime} value the date time value to pack.
142+
* @param {Packer} packer the packer to use.
143+
*/
144+
function packDateTimeWithZoneId (value, packer) {
145+
146+
const epochSecond = localDateTimeToEpochSecond(
147+
value.year,
148+
value.month,
149+
value.day,
150+
value.hour,
151+
value.minute,
152+
value.second,
153+
value.nanosecond
154+
)
155+
156+
const offset = value.timeZoneOffsetSeconds != null
157+
? value.timeZoneOffsetSeconds
158+
: getOffsetFromZoneId(value.timeZoneId, epochSecond, value.nanosecond)
159+
160+
const utc = epochSecond.subtract(offset)
161+
const nano = int(value.nanosecond)
162+
const timeZoneId = value.timeZoneId
163+
164+
const packableStructFields = [
165+
packer.packable(utc),
166+
packer.packable(nano),
167+
packer.packable(timeZoneId)
168+
]
169+
packer.packStruct(DATE_TIME_WITH_ZONE_ID, packableStructFields)
170+
}
171+
172+
/**
173+
* Pack given date time with zone offset.
174+
* @param {DateTime} value the date time value to pack.
175+
* @param {Packer} packer the packer to use.
176+
*/
177+
function packDateTimeWithZoneOffset (value, packer) {
178+
const epochSecond = localDateTimeToEpochSecond(
179+
value.year,
180+
value.month,
181+
value.day,
182+
value.hour,
183+
value.minute,
184+
value.second,
185+
value.nanosecond
186+
)
187+
const nano = int(value.nanosecond)
188+
const timeZoneOffsetSeconds = int(value.timeZoneOffsetSeconds)
189+
const utcSecond = epochSecond.subtract(timeZoneOffsetSeconds)
190+
191+
const packableStructFields = [
192+
packer.packable(utcSecond),
193+
packer.packable(nano),
194+
packer.packable(timeZoneOffsetSeconds)
195+
]
196+
packer.packStruct(DATE_TIME_WITH_ZONE_OFFSET, packableStructFields)
197+
}
198+
199+
200+
/**
201+
* Returns the offset for a given timezone id
202+
*
203+
* Javascript doesn't have support for direct getting the timezone offset from a given
204+
* TimeZoneId and DateTime in the given TimeZoneId. For solving this issue,
205+
*
206+
* 1. The ZoneId is applied to the timestamp, so we could make the difference between the
207+
* given timestamp and the new calculated one. This is the offset for the timezone
208+
* in the utc is equal to epoch (some time in the future or past)
209+
* 2. The offset is subtracted from the timestamp, so we have an estimated utc timestamp.
210+
* 3. The ZoneId is applied to the new timestamp, se we could could make the difference
211+
* between the new timestamp and the calculated one. This is the offset for the given timezone.
212+
*
213+
* Example:
214+
* Input: 2022-3-27 1:59:59 'Europe/Berlin'
215+
* Apply 1, 2022-3-27 1:59:59 => 2022-3-27 3:59:59 'Europe/Berlin' +2:00
216+
* Apply 2, 2022-3-27 1:59:59 - 2:00 => 2022-3-26 23:59:59
217+
* Apply 3, 2022-3-26 23:59:59 => 2022-3-27 00:59:59 'Europe/Berlin' +1:00
218+
* The offset is +1 hour.
219+
*
220+
* @param {string} timeZoneId The timezone id
221+
* @param {Integer} epochSecond The epoch second in the timezone id
222+
* @param {Integerable} nanosecond The nanoseconds in the timezone id
223+
* @returns The timezone offset
224+
*/
225+
function getOffsetFromZoneId (timeZoneId, epochSecond, nanosecond) {
226+
const dateTimeWithZoneAppliedTwice = getTimeInZoneId(timeZoneId, epochSecond, nanosecond)
227+
228+
// The wallclock form the current date time
229+
const epochWithZoneAppliedTwice = localDateTimeToEpochSecond(
230+
dateTimeWithZoneAppliedTwice.year,
231+
dateTimeWithZoneAppliedTwice.month,
232+
dateTimeWithZoneAppliedTwice.day,
233+
dateTimeWithZoneAppliedTwice.hour,
234+
dateTimeWithZoneAppliedTwice.minute,
235+
dateTimeWithZoneAppliedTwice.second,
236+
nanosecond)
237+
238+
const offsetOfZoneInTheFutureUtc = epochWithZoneAppliedTwice.subtract(epochSecond)
239+
const guessedUtc = epochSecond.subtract(offsetOfZoneInTheFutureUtc)
240+
241+
const zonedDateTimeFromGuessedUtc = getTimeInZoneId(timeZoneId, guessedUtc, nanosecond)
242+
243+
const zonedEpochFromGuessedUtc = localDateTimeToEpochSecond(
244+
zonedDateTimeFromGuessedUtc.year,
245+
zonedDateTimeFromGuessedUtc.month,
246+
zonedDateTimeFromGuessedUtc.day,
247+
zonedDateTimeFromGuessedUtc.hour,
248+
zonedDateTimeFromGuessedUtc.minute,
249+
zonedDateTimeFromGuessedUtc.second,
250+
nanosecond)
251+
252+
const offset = zonedEpochFromGuessedUtc.subtract(guessedUtc)
253+
return offset
254+
}
255+
256+
function getTimeInZoneId (timeZoneId, epochSecond, nano) {
257+
const formatter = new Intl.DateTimeFormat('en-US', {
258+
timeZone: timeZoneId,
259+
year: 'numeric',
260+
month: 'numeric',
261+
day: 'numeric',
262+
hour: 'numeric',
263+
minute: 'numeric',
264+
second: 'numeric',
265+
hour12: false
266+
})
267+
268+
const l = epochSecondAndNanoToLocalDateTime(epochSecond, nano)
269+
const utc = Date.UTC(
270+
int(l.year).toNumber(),
271+
int(l.month).toNumber() - 1,
272+
int(l.day).toNumber(),
273+
int(l.hour).toNumber(),
274+
int(l.minute).toNumber(),
275+
int(l.second).toNumber()
276+
)
277+
278+
const formattedUtcParts = formatter.formatToParts(utc)
279+
280+
const localDateTime = formattedUtcParts.reduce((obj, currentValue) => {
281+
if (currentValue.type !== 'literal') {
282+
obj[currentValue.type] = int(currentValue.value)
283+
}
284+
return obj
285+
}, {})
286+
287+
const epochInTimeZone = localDateTimeToEpochSecond(
288+
localDateTime.year,
289+
localDateTime.month,
290+
localDateTime.day,
291+
localDateTime.hour,
292+
localDateTime.minute,
293+
localDateTime.second,
294+
localDateTime.nanosecond
295+
)
296+
297+
localDateTime.timeZoneOffsetSeconds = epochInTimeZone.subtract(epochSecond)
298+
localDateTime.hour = localDateTime.hour.modulo(24)
299+
300+
return localDateTime
301+
}
302+
303+
304+
function convertIntegerPropsIfNeeded (obj, disableLosslessIntegers, useBigInt) {
305+
if (!disableLosslessIntegers && !useBigInt) {
306+
return obj
307+
}
308+
309+
const convert = value =>
310+
useBigInt ? value.toBigInt() : value.toNumberOrInfinity()
311+
312+
const clone = Object.create(Object.getPrototypeOf(obj))
313+
for (const prop in obj) {
314+
if (Object.prototype.hasOwnProperty.call(obj, prop) === true) {
315+
const value = obj[prop]
316+
clone[prop] = isInt(value) ? convert(value) : value
317+
}
318+
}
319+
Object.freeze(clone)
320+
return clone
321+
}
322+
323+

0 commit comments

Comments
 (0)