Skip to content

Commit 89a8972

Browse files
authored
Implementing new structures for DateTime (#953)
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 un-existing or ambiguous 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 back-port of #948
1 parent 1a1c11c commit 89a8972

File tree

14 files changed

+1103
-30
lines changed

14 files changed

+1103
-30
lines changed

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

+47
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

+4-1
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],
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)